Home > Software engineering >  multiple optional members in class template without overhead
multiple optional members in class template without overhead

Time:09-22

If I want a class with an optional member, I'm using template specialization:

template<class T>
struct X {
  T t;
  void print() { cout << "t is " << t << '\n'; }
};
template<>
struct X<void> {
  void print() { cout << "without T\n"; }
};

This is nice as there is no runtime overhead and little code duplication. However, if I have 3 instead of 1 optional class member, I have to write 2^3=8 classes, that is, the "little duplication" quickly becomes unreasonable.

A possible solution may be to use std::conditional like so:

template<class T1, class T2, class T3>
struct X {
  conditional_t<is_void_v<T1>, char, T1> t1;
  conditional_t<is_void_v<T2>, char, T2> t2;
  conditional_t<is_void_v<T3>, char, T3> t3;

  void print() {
    if constexpr (!is_void_v<T1>) cout << "t1 is " << t1 << '\n';
    if constexpr (!is_void_v<T2>) cout << "t2 is " << t2 << '\n';
    if constexpr (!is_void_v<T3>) cout << "t3 is " << t3 << '\n';
  }
};

but now, objects of my class waste memory. I'm looking for some way to avoid most of the code duplication (down to at most linear in the number of optional members code overhead) while avoiding to spend more runtime and memory than necessary.

Note that this question exists (with answers) for single optional class members (see Optional class members without runtime overhead, Most efficient way to implement template-based optional class members in C ?) but, to the best of my knowledge, it has not been answered for multiple optional members.

CodePudding user response:

With an optional_member class,

template <class T>
struct OptionalMember
{
    T t;
    static constexpr bool has_member = true;
};

template<> struct X<void>
{
    static constexpr bool has_member = false;
};

you might use inheritance (as long as their types differ) and EBO to avoid extra memory.

template<class T1, class T2, class T3>
struct X : OptionalMember<T1>, OptionalMember<T2>, OptionalMember<T3>
{
  void print() {
    if constexpr (!OptionalMember<T1>::has_member)
        cout << "t1 is " << OptionalMember<T1>::t << '\n';
    if constexpr (!OptionalMember<T2>::has_member)
        cout << "t2 is " << OptionalMember<T2>::t << '\n';
    if constexpr (!OptionalMember<T1>::has_member)
        cout << "t3 is " << OptionalMember<T3>::t << '\n';
  }
};

or, since C 20, attribute [[no_unique_address]] (no extra memory as long as their types differ).

template<class T1, class T2, class T3>
struct X {
  [[no_unique_address]] OptionalMember<T1> t1;
  [[no_unique_address]] OptionalMember<T2> t2;
  [[no_unique_address]] OptionalMember<T3> t3;

  void print() {
    if constexpr (!t1.has_member) cout << "t1 is " << t1.t << '\n';
    if constexpr (!t2.has_member) cout << "t2 is " << t2.t << '\n';
    if constexpr (!t3.has_member) cout << "t3 is " << t3.t << '\n';
  }
};

If type might be identical, you might modify OptionalMember to take extra tag (or any kind of identifier as std::size_t):

template <class T, class Tag>
struct OptionalMember
{
    T t;
    static constexpr bool has_member = true;
};

template<class Tag> struct X<void, Tag>
{
    static constexpr bool has_member = false;
};

and then

struct tag1;
struct tag2;
struct tag3;

and use OptionalMember<TX, tagX> instead of OptionalMember<TX> from above solution.

CodePudding user response:

It is possible to use inheritance to only write 2*3=6 classes and separate the data from the actual code using it:

template<class T1> struct optional_tuple1 {T1 first;};
template<> struct optional_tuple1<void> {};

template<class T1, class T2> struct optional_tuple2: public optional_tuple1<T1> { T2 second;};
template<class T1> struct optional_tuple2<T1, void>: public optional_tuple1<T1> {};

template<class T1, class T2, class T3> struct optional_tuple3: public optional_tuple2<T1, T2> {T3 third;};
template<class T1, class T2> struct optional_tuple3<T1, T2, void>: public optional_tuple2<T1, T2> {};

However, you have to make sure that you're not using a variable second if T2 is void. This can be achieved with if constexpr (!std::is_void_v<T2>) (as above):

template<class T1, class T2, class T3>
struct X3: public optional_tuple3<T1, T2, T3> {
  void print() {
    if constexpr (!is_void_v<T1>) cout << "first is " << this->first << '\n';
    if constexpr (!is_void_v<T2>) cout << "second is " << this->second << '\n';
    if constexpr (!is_void_v<T3>) cout << "third is " << this->third << '\n';
  }
};

NOTE: this is only optimal if not all Ts are void since, if I recall correctly, classes cannot have size-0 in C (I'll assume because no 2 objects may receive the same address in memory).

CodePudding user response:

How about something like this:

template<typename T>
struct member_haver {
    T m_member;
};

template<>
struct member_haver<void> {
};

template<typename... Ts>
struct X : member_haver<Ts>... {
    void print() {
        ((std::cout << member_haver<Ts>::m_member << ", "), ...);
    }
};

Then, for example, you could say:

int main() {
    X<int, void, float> x{};
    x.member_haver<int>::m_member = 1;
    x.member_haver<float>::m_member = 5.2f;

    x.print();

    return 0;
}

Of course this would probably need to be syntactically refined, but the gist is there.

  • Related