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;
wrapped() = default;
template <class Arg>
wrapped(Arg&& arg) : val(std::forward<Arg>(arg)) { }
};
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;
Base& get(typelist<Cond, T> ) { return static_cast<Base&>(*this); }
Base const & get(typelist<Cond, T> ) const { return static_cast<const Base&>(*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(typelist<std::is_same<T, int>, int>{}).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.