C++ Restricting variable type to multiple base classes

Viewed 79

Consider the following classes:

class A {};
class B {};
class C {};

class D : public A, public B, public C {};
class E : public A, public C {};
class F : public A {};

I want to write a variable type that only accepts types which derive from both A and B (in this case only D) so that the following hold:

T var;
var = D(); // valid
var = E(); // invalid, does not derive from B
var = F(); // invalid, does not derive from B

I could do the following and use G as my type. G would act as a sort of 'type grouper'.

class G : public A, public B {};

class D : public F, public C {};

However if I want to apply this to any combination of base types or with any number of base types, I would need a large number of these type groupers. Is there a way to implement a multiple base type requirement without manually generating type groupers like I showed?

I do not have a specific use case that requires this, I am purely curious whether this is possible.

EDIT: Accidentally used F twice. Switched to G for the second time.

3 Answers

Similar to this answer, but using constraints and concepts in C++20.

#include <concepts>

class A {};
class B {};
class C {};

class D : public A, public B, public C {};
class E : public A, public C {};
class F : public A {};

template<class T>
concept AB = std::derived_from<T, A> && std::derived_from<T, B>;

template<AB T>
T *newAB() {
    return new T;
}

int main() {
    auto d = newAB<D>();
    auto e = newAB<E>(); // error: 'E' does not satisfy 'AB'
    auto f = newAB<F>(); // error: 'F' does not satisfy 'AB'
}

If using C++11 can do this. Later versions offer some tweaks.

The closest I think you can get is using is_base_of and static_asserts, or a function which checks both with static_assert. e.g.

#include <type_traits>

class A {};
class B {};
class C {};
  
template <class T>
void CheckClassesAreDerivedFromAandB()
{
    static_assert(std::is_base_of<A, T>::value, "Check we are derived from A");
    static_assert(std::is_base_of<B, T>::value, "Check we are derived from B");
}

template <class T>
void CheckClassesAreDerivedFromAandB(const T&)
{
    static_assert(std::is_base_of<A, T>::value, "Check we are derived from A");
    static_assert(std::is_base_of<B, T>::value, "Check we are derived from B");
}

class D : public A, public B, public C {};
class E : public A, public C {};
class F : public A {}; 

void test()
{
    CheckClassesAreDerivedFromAandB<D>();
    
    D d;
    CheckClassesAreDerivedFromAandB(d);
}

You could even combine this into a factory method.

template <class T>
T* Create()
{
    CheckBaseClassesAreDerivedFromAandB<T>();
    return new T();
}

template <class T>
T Build()
{
    CheckBaseClassesAreDerivedFromAandB<T>();
    return T();
}
void test()
{
    auto d = Create<D>();
    auto e = Create<E>(); // compile error here
    auto another_d = Build<D>();
    ...
}

You Here is a type that references (points to) an object that derives from both A and B. The simplest barebones version is

struct AB
{
  A* a;
  B* b;
  template <class D> AB(D* d) : a(d), b(d) {};
  template <class D> AB& operator=(D* d) { a=d; b=d; return *this;}
};

Now you can have:

D d;
E e;
AB ab1(&d); // OK
AB ab2(&e); // Not OK

This can be templatized and expanded:

template <typename ... As>
struct Combine
{
   std::tuple<As * ...> as;
   template <class D> As(D* d) : as(replicate<sizeof...(As)>(d) {};
   // etc etc --- writing `replicate` is left ans an exercise
};

One could add data hiding, accessors, static asserts, concepts, smart pointers and whatnot on top of this, but the idea is still the same. If you derive from A and B, you have an A part and a B part. Let me reference both.

Providing a pair of pointers/references to A and to B is the only reasonable interface one could expect from such a type (without knowing specific details about A and B).

Ostensibly one can also have a type that copies, rather than references.

struct CopyingAB : A, B
{
  template <class D> CopyingAB(const D& d) : A(d), B(d) {};
  template <class D> CopyingAB& operator=(const D& d) 
    { 
      A::operator=(d); 
      B::operator=(d); 
      return *this;
    }
};

But you should never want or need this, because object slicing is a bug, not a feature.

There are probably no real use cases for the referencing/pointing version either, but it is not inherently evil in and of itself, so let it be.

Related