Skip to content

Classes and Structs

Hannes Hauswedell edited this page Mar 31, 2017 · 19 revisions

Preface

Definitions

In SeqAn3 differentiate between the following groups of class types:

  1. 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
  2. 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
  3. 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 or private (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":

  1. certain metafunctions:
  2. 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

Use-cases

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.

Examples

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;
};

General Design

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 (via concepts)

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;
}

Dynamic polymorphism

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.

Clone this wiki locally