-
Notifications
You must be signed in to change notification settings - Fork 82
Classes and Structs
In SeqAn3 differentiate between the following groups of class types:
-
aggregate POD class types:
- satisfy both the PODType definition and support aggregate initialization
- denoted by the
struct
keyword - no constructor/destructor definitions at all
- data members should be prefixed with
_
to mark them as "private" - data members writable through aggregate initialization, assignment operator and/or setter functions
- data members readable through structured bindings and/or getter functions
-
non-aggregate POD class types:
- satisfy the PODType definition, but don't support aggregate initialization
- denoted by the
class
keyword - should have the rule-of-six constructors/destructors/assignment-ops explicitly defaulted
- have additional constructors
- data members should be prefixed with
_
to mark them as "private" - data members writable through constructors, assignment operators and/or setter functions
- data members readable through getter functions
-
non POD class types:
- everything else that stores data
- should explicitly declare rule-of-six constructors/destructors/assignment-ops (can explicitly default or delete)
- denoted by the
class
keyword - data members should usually be
protected
orprivate
(don't need to be prefixed then) - data members writable through constructors, assignment operators and/or setter functions
- data members readable through getter functions
- member functions should never be virtual, see Dynamic and Static Polymorphism for more details.
In addition there are constructs implemented as classes that don't "store data":
- certain metafunctions:
-
traits types:
- only contain other type declarations and
static const
/static constexpr
data members - used to specialize the behaviour of other non POD class types
- denoted by
struct
keyword and a_traits
suffix in the name
- only contain other type declarations and
Whenever you have something that is stored in a container and/or copied around frequently, it might profit from being a POD type vs a non-POD type, e.g. our alphabet concept requires alphabets to be POD types. If you implement a POD type, check whether you can also make it an aggregate type so that you can avoid boilerplate code and improve readability of the data structure, e.g. the dna4
alphabet is an aggregate POD type, but the compound_alphabet_u
is a non-aggregate POD type.
If you have a general purpose data structure that deals with more complex data, needs encapsulation and/or customized construction behaviour, opt for the most general of the above types.
Note that there are other types, as well (e.g. non-POD aggregate types), but that we explicitly require you choose one of the distinctions above.
struct aggr_pod_type
{
char _c;
char get_c() const
{
return _c;
}
void set_c(char const c)
{
_c = c;
}
};
class non_aggr_pod_type
{
// all public
public:
char _c;
// Rule-of-six explicitly defaulted/deleted, () may not be deleted
non_aggr_pod_type() = default;
constexpr non_aggr_pod_type(non_aggr_pod_type const &) = default;
constexpr non_aggr_pod_type(non_aggr_pod_type &&) = default;
constexpr non_aggr_pod_type & operator =(non_aggr_pod_type const &) = default;
constexpr non_aggr_pod_type & operator =(non_aggr_pod_type &&) = default;
constexpr ~non_aggr_pod_type() = default;
// user defined constructors
non_aggr_pod_type(char const c) :
_c{c}
{}
// silly example:
non_aggr_pod_type(std::string const & s) :
_c{s[0]}
{}
char get_c() const
{
return _c;
}
void set_c(char const c)
{
_c = c;
}
};
class non_aggr_non_pod_type
{
// always specify public, protected, private in this order
public:
// Rule-of-six all explicitly listed, some non-defaulted
non_aggr_pod_type() :
c{'a'}
{}
constexpr non_aggr_pod_type(non_aggr_pod_type const &) = default;
constexpr non_aggr_pod_type(non_aggr_pod_type &&) = default;
constexpr non_aggr_pod_type & operator =(non_aggr_pod_type const &) = default;
constexpr non_aggr_pod_type & operator =(non_aggr_pod_type &&) = default;
constexpr ~non_aggr_pod_type() = default;
// user defined constructors
non_aggr_pod_type(char const _c) :
c{_c}
{}
char get_c() const
{
return c;
}
void set_c(char const _c)
{
c = _c;
}
protected:
char c;
};
POD/Aggregate preferred (shall be struct
, otherwise shall be class
and always specify rule of 5/6 constructors (ok to = default
or = deleted
, but always make explicit).
struct my_aggregate_type
{
int my_member{7}; // use direct member initializers if necessary
};
class my_non_aggregate_type
{
// always specify public, protected, private in this order
public:
// Rule-of-six is usually desired
my_non_aggregate_type() = default;
// Rule-of-five
constexpr my_non_aggregate_type(my_non_aggregate_type const &) = default;
constexpr my_non_aggregate_type(my_non_aggregate_type &&) = default;
constexpr my_non_aggregate_type & operator =(my_non_aggregate_type const &) = default;
constexpr my_non_aggregate_type & operator =(my_non_aggregate_type &&) = default;
protected:
// make destructor protected to prevent dynamic polymorphism (unless explicitly desired)
constexpr ~my_non_aggregate_type() = default;
private:
int my_member{7}; // use direct member initializers if necessary
};
Static polymorphism is implemented via C++ concepts. Instead of designing a class, you first abstract the class's public interface and make a concept of it. Algorithms and other free function than work on general types (as templates) that are constrained via more or less specialized concepts.
#include <iostream>
#include <type_traits>
template<typename number_type>
concept bool number_concept = std::is_arithmetic<T>::value;
template<typename number_type>
concept bool integral_concept = number_concept<T> && std::is_integral<T>::value;
template<typename number_type>
requires number_concept<number_type>
number_type double_the_value(number_type const in)
{
std::cerr << "general funcion!\n";
return in * 2;
}
template<typename number_type>
requires integral_concept<number_type>
number_type double_the_value(number_type const in)
{
std::cerr << "special funcion!\n";
return (in << 1);
}
template<typename number_type>
requires number_concept<number_type>
void print2(number_type const & v)
{
std::cout << double_the_value(v) << '\n';
}
int main()
{
print2(double{0.2}); // general case
print2(int{2}); // specialized case
print2(long{2}); // specialized case
return 0;
}
In some cases you just don't know which function can be picked at compile-time, e.g. the exact input-file type cannot be known. In these cases you can use an tag-dispatching over an inner type that also follows a concept for these specializations. Tag-dispatching has become quite easy in C++17 with std::visit.