I have the following situation (live code : https://gcc.godbolt.org/z/d8jG9bs9a):
#include <iostream>
#include <type_traits>
#define ENBALE true // to enable disable test solutions
enum struct Type : unsigned { base = 0, child1, child2, child3 /* so on*/ };
// CRTP Base
template<typename Child> struct Base {
void doSomething() { static_cast<Child*>(this)->doSomething_Impl(); }
private:
Base() = default;
friend Child;
};
struct Child1 : public Base<Child1> {
void doSomething_Impl() { std::cout << "Child1 implementation\n"; }
};
struct Child2 : public Base<Child2> {
void doSomething_Impl() { std::cout << "Child2 implementation\n"; }
};
struct Child3 : public Base<Child3> {
void doSomething_Impl() { std::cout << "Child3 implementation\n"; }
};
// ... so on
class SomeLogicClass
{
Type mClassId{ Type::base };
Child1 mChild1;
Child2 mChild2;
Child3 mChild3;
public:
Type getId() const { return mClassId; }
void setId(Type id) { mClassId = id; } // run time depended!
#if ENBALE // Solution 1 : simple case
/*what in C 11?*/ getInstance()
{
switch (mClassId)
{
case Type::child1: return mChild1;
case Type::child2: return mChild2;
case Type::child3: return mChild3;
default: // error case!
break;
}
}
#elif !ENBALE // Solution 2 : SFINAE
template<Type ID>
auto getInstance() -> typename std::enable_if<ID == Type::child1, Child1&>::type { return mChild1; }
template<Type ID>
auto getInstance() -> typename std::enable_if<ID == Type::child2, Child2&>::type { return mChild2; }
template<Type ID>
auto getInstance() -> typename std::enable_if<ID == Type::child3, Child3&>::type { return mChild3; }
#endif
};
void test(SomeLogicClass& ob, Type id)
{
ob.setId(id);
#if ENBALE // Solution 1
auto& childInstance = ob.getInstance();
#elif !ENBALE // Solution 2
auto& childInstance = ob.getInstance<ob.getId()>();
#endif
childInstance.doSomething(); // calls the corresponding implementations!
}
int main()
{
SomeLogicClass ob;
test(ob, Type::child1);
test(ob, Type::child2);
test(ob, Type::child3);
}
The problem is that the child class selection (to which the doSomething_Impl()
must be called), should be taken place by deciding upon a run time variable mClassId
of the SomeLogicClass
.
The only two possible solutions I can think of are a normal switch case and SFINAE the member functions, as described in the above minimal example. As noted in the comments in the above code, both can't get work, for the reasons
- Solution 1: the member function must have a unique return type
- Solution 2: SFINAE required a compile time expression to decide which overload to be chosen.
Update
The std::variant
(as mentioned by @lorro) would be the easiest solution here. However, require C 17 support.
However, I would like to know if we got some way around which works under the compiler flag c 11?
Note: I am working with a code-base, where external libs such as boost, can not be used, and the CRTP class structure is mostly untouchable.
CodePudding user response:
It is not clear if you want one element or one of each.
If you need one of each, SomeLogicClass should have a std::tuple<Class1, Class2, Class3>
. This stores 'one of each', also provides std::get<>()
, which returns element i
. Thus, getInstance()
can essentially delegate to std::get<ID>()
.
If you need one element of either type, SomeLogicClass should have a std::variant<Class1, Class2, Class3>
. This stores the kind of the active element and the element itself. You can either use std::get<>()
for it, or - better - you might visit via std::visit()
.
CodePudding user response:
Since you are restricted to C 11 and are not allowed to use external libraries such as boost::variant
, an alternative would be to reverse the logic: Do not attempt to return the child type but instead pass in the operation to perform on the child. Your example could become this (godbolt):
#include <iostream>
#include <type_traits>
enum struct Type : unsigned { base = 0, child1, child2, child3 /* so on*/ };
// CRTP Base
template<typename Child> struct Base {
void doSomething() { static_cast<Child*>(this)->doSomething_Impl(); }
private:
Base() = default;
friend Child;
};
struct Child1 : public Base<Child1> {
void doSomething_Impl() { std::cout << "Child1 implementation\n"; }
};
struct Child2 : public Base<Child2> {
void doSomething_Impl() { std::cout << "Child2 implementation\n"; }
};
struct Child3 : public Base<Child3> {
void doSomething_Impl() { std::cout << "Child3 implementation\n"; }
};
// ... so on
class SomeLogicClass
{
Type mClassId{ Type::base };
Child1 mChild1;
Child2 mChild2;
Child3 mChild3;
// ... child3 so on!
public:
Type getId() const { return mClassId; }
void setId(Type id) { mClassId = id; } // run time depended!
template <class Func>
void apply(Func func)
{
switch (mClassId){
case Type::child1: func(mChild1); break;
case Type::child2: func(mChild2); break;
case Type::child3: func(mChild3); break;
default: // error case!
break;
}
}
};
struct DoSomethingCaller
{
template <class T>
void operator()(T & childInstance){
childInstance.doSomething();
}
};
void test(SomeLogicClass& ob, Type id)
{
ob.setId(id);
ob.apply(DoSomethingCaller{});
// Starting with C 14, you can also simply write:
//ob.apply([](auto & childInstance){ childInstance.doSomething(); });
}
int main()
{
SomeLogicClass ob;
test(ob, Type::child1);
test(ob, Type::child2);
test(ob, Type::child3);
}
Notice how the new function apply()
replaces your getInstance()
. But instead of attempting to return the child type, it accepts some generic operation that it applies to the correct child. The functor passed in needs to cope (i.e. compile) with all possible child types. Since all of them have a doSomething()
method, you can simply use a templated functor (DoSomethingCaller
). Before C 14, unfortunately, it cannot simply be a polymorphic lambda but needs to be a proper struct (DoSomethingCaller
) outside of the function.
If you care to do so, you can also restrict DoSomethingCaller
to the CRTP base class Base<T>
:
struct DoSomethingCaller
{
template <class T>
void operator()(Base<T> & childInstance){
childInstance.doSomething();
}
};
which might make it a bit more readable.
Depending on how strict the "no external libraries" restriction is, maybe only boost is not allowed but a single external header (that can be simply included in the code base as any other header file) would be possible? If yes, you might want to also have a look at variant-lite. It aims to be a C 98/C 11 compatible replacement for std::variant
.