9
\$\begingroup\$

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<T> and optional_base<T> useful:

namespace xstd {
namespace block_adl {

template<class>
struct empty_base
{
        empty_base() = default;

        template<class... Args>
        constexpr empty_base(Args&&...) {}
};

template<bool Condition, class T>
using optional_base = std::conditional_t<Condition, T, empty_base<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 T>
class Test
:
    public Base0, // unconditional base class
    public xstd::optional_base<Trait1_v<T>, Base1>,
    public xstd::optional_base<Trait2_v<T>, Base2>
{
    using B1 = xstd::optional_base<Trait1_v<T>, Base1>;
    using B2 = xstd::optional_base<Trait2_v<T>, 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.

Obviously, over-use of inheritance has to be avoided, but the above design does avoid quite a lot of code duplication. Oh, and I haven't tested this with virtual functions, virtual base classes and what-not. I just use this for assembling value-semantic classes out of conditional building blocks.

Question: what other design / usability issues could there be with the above code?

\$\endgroup\$
2
  • \$\begingroup\$ Could you provide an example of how you'd expect the user to define Trait1_v? \$\endgroup\$
    – sunny
    Commented Aug 21, 2015 at 9:28
  • \$\begingroup\$ @sunny e.g. template<class T> constexpr auto Trait1_v = std::is_integral<T>::value; (in C++14 you can have variable templates) \$\endgroup\$ Commented Aug 21, 2015 at 9:43

1 Answer 1

7
+50
\$\begingroup\$

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

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

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

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 T>
class Test
: private xstd::optional_base<Trait1_v<T>, int, 0>
, private xstd::optional_base<Trait2_v<T>, 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 <typename... >
struct typelist { };

template <int, typename... >
struct OptionalBasesImpl;

The root case is empty:

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

    void get(typelist<> ) { }
};

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

template <int ID, typename Cond, typename T, typename... Rest>
struct OptionalBasesImpl<ID, Cond, T, Rest...>
: private xstd::optional_base<Cond::value, T, ID>
, OptionalBasesImpl<ID + 1, Rest...>
{
    using Base = xstd::optional_base<Cond::value, T, ID>;
    using Root = OptionalBasesImpl<ID + 1, Rest...>;

    OptionalBasesImpl() = default;

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

protected:
    using Root::get;

    // by conditional/type pair
    Base& get(typelist<Cond, T> ) { return *this; }
    Base const& get(typelist<Cond, T> ) const { return *this; }

    // by index
    Base& get(std::integral_constant<int, ID> ) { return *this; } 
    Base const& get(std::integral_constant<int, ID> ) const { return *this; }
};

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

template <typename... T>
struct OptionalBases : OptionalBasesImpl<0, T...> {
protected:
    using OptionalBasesImpl<0, T...>::OptionalBasesImpl;
    using OptionalBasesImpl<0, T...>::get;
};

which lets you write something like:

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

    void printFirst() {
        std::cout << Bases::get(typelist<std::is_integral<T>, int>{}).val << std::endl;
    }

    void printSecond() {
        std::cout << Bases::get(std::integral_constant<int, 1>{}).val << std::endl;
    }
};

int main() {
    Test<int> t{1, 2};
    t.printFirst();
    t.printSecond();
}

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

\$\endgroup\$
1
  • \$\begingroup\$ Awesome answer! BTW, I use public inheritance because I have used this to optionally add stateful mixins (i.e with their own behavorial abstraction) instead of plain data. So the data that is inherited is actually already private, and the public inheritance is to expose the member functions. I haven't encountered the need for multiple inclusions of the same base, but it could happen by accident in generic code, so thanks for your example. \$\endgroup\$ Commented Sep 6, 2015 at 20:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.