HiveBrain v1.2.0
Get Started
← Back to all entries
patterncppMinor

Optional base class template to get conditional data members

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
templatedataconditionaloptionalgetmembersclassbase

Problem

In generic code, I sometimes want to conditionally add a data member to a class template. Writing separate template specializations scales as 2^N for N conditional data members. Absent a static_if feature, I have found the following user-defined class templates empty_base and optional_base useful:

namespace xstd {
namespace block_adl {

template
struct empty_base
{
        empty_base() = default;

        template
        constexpr empty_base(Args&&...) {}
};

template
using optional_base = std::conditional_t>;

}       // namespace block_adl

// deriving from empty_base or optional_base will not make xstd an associated namespace
// this prevents ADL from finding overloads for e.g. begin/end/swap from namespace xstd
using block_adl::empty_base;
using block_adl::optional_base;

}       // namespace xstd


Exploiting the empty base optimization then allows users to write class templates that have optional data members (conditional on user-defined template variable traits Trait1_v and Trait2_v)

template
class Test
:
    public Base0, // unconditional base class
    public xstd::optional_base, Base1>,
    public xstd::optional_base, Base2>
{
    using B1 = xstd::optional_base, Base1>;
    using B2 = xstd::optional_base, Base2>;
public:
    Test() = default;

    Test(Arg0 const& a0, Arg1 const& a1, Arg2 const& a2)
    :
        Base0(a0),
        B1(a1),
        B2(a2)
    {}    
};


Note that the optional_base variadic constructor allows linear scaling of constructor delegation in the user-defined class template Test. One minor wart is that this constructor does require passing all parameters (but otherwise, 2^N different constructors have to be written). But if any of the traits variables evaluates to false, the corresponding parameter and call to the base constructor will be optimized away. And because optional_base also has a trivial default constructor, POD-ness of user-defined classes is preserved by inheriting from it.

Obvio

Solution

I only have a few comments. First, you probably want to inherit your bases privately:

template
class Test
:
    private Base0, // unconditional base class
    private xstd::optional_base, Base1>,
    private xstd::optional_base, Base2>
{ };


After all, your design is about adding data members. That's something that you probably don't want to expose to the outside world.

Secondly, what happens if you want to potentially have multiple different members of the same type? That's definitely going to come up and so should be supported. In order to do that, you might want to add some kind of ID to differentiate them and then additionally wrap the non-empty case. That is:

template 
struct empty_base { ... }; // same as before

template 
struct wrapped {
    T val;
};  

template 
using optional_base = std::conditional_t,
                                         empty_base>;


This will additionally let you use non-class types as optional data members (since you couldn't inherit from, e.g. int). So you can now do something like:

template 
class Test
: private xstd::optional_base, int, 0>
, private xstd::optional_base, int, 1>
{ ... };


That adds an inconvenience that you have to explicitly add the sequence 0, 1, ...

You can get around that by making a linear inheritance list of all your optional bases. I'm not sure if it's worth it, but I'll present the idea just as an option. At its core, we have OptionalBasesImpl which will inherit from all the optional bases and provide a getter so that the derived class can actually use them:

template 
struct typelist { };

template 
struct OptionalBasesImpl;


The root case is empty:

template 
struct OptionalBasesImpl
{
    OptionalBasesImpl() = default;
    template 
    OptionalBasesImpl(Args&&... ) { }

    void get(typelist<> ) { }
};


And we recurse over each pair of type trait/type:

template 
struct OptionalBasesImpl
: private xstd::optional_base
, OptionalBasesImpl
{
    using Base = xstd::optional_base;
    using Root = OptionalBasesImpl;

    OptionalBasesImpl() = default;

    template 
    OptionalBasesImpl(Arg&& a0, Args&&... args)
    : Base{std::forward(a0)}
    , Root(std::forward(args)...)
    { }

protected:
    using Root::get;

    // by conditional/type pair
    Base& get(typelist ) { return *this; }
    Base const& get(typelist ) const { return *this; }

    // by index
    Base& get(std::integral_constant ) { return *this; } 
    Base const& get(std::integral_constant ) const { return *this; }
};


Now we just have the top-level class to start us off at 0:

template 
struct OptionalBases : OptionalBasesImpl {
protected:
    using OptionalBasesImpl::OptionalBasesImpl;
    using OptionalBasesImpl::get;
};


which lets you write something like:

template 
struct Test
: private OptionalBases, int,
                        std::is_same, int>
{
    using Bases = OptionalBases, int,
                        std::is_same, int>;
    using Bases::Bases;

    void printFirst() {
        std::cout , int>{}).val {}).val  t{1, 2};
    t.printFirst();
    t.printSecond();
}


That's a lot of extra boilerplate for just avoiding writing a sequence of integers, so YMMV.

Code Snippets

template<class T>
class Test
:
    private Base0, // unconditional base class
    private xstd::optional_base<Trait1_v<T>, Base1>,
    private xstd::optional_base<Trait2_v<T>, Base2>
{ };
template <class, int >
struct empty_base { ... }; // same as before

template <class T, int >
struct wrapped {
    T val;
};  

template <bool Condition, class T, int UniqueID = 0>
using optional_base = std::conditional_t<Condition,
                                         wrapped<T, UniqueID>,
                                         empty_base<T, UniqueID>>;
template <class T>
class Test
: private xstd::optional_base<Trait1_v<T>, int, 0>
, private xstd::optional_base<Trait2_v<T>, int, 1>
{ ... };
template <typename... >
struct typelist { };

template <int, typename... >
struct OptionalBasesImpl;
template <int ID>
struct OptionalBasesImpl<ID>
{
    OptionalBasesImpl() = default;
    template <typename... Args>
    OptionalBasesImpl(Args&&... ) { }

    void get(typelist<> ) { }
};

Context

StackExchange Code Review Q#101541, answer score: 7

Revisions (0)

No revisions yet.