How to convert/use the run time variable in compile time expression?
Asked Answered
B

3

7

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 ?

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.

Blackthorn answered 14/8, 2022 at 21:2 Comment(0)
L
4

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.

Lixiviate answered 17/8, 2022 at 6:15 Comment(0)
W
3

I recognize here the Bridge pattern in your CRTP code base, although done badly in my opinion. But anyway, if you can't touch that code then you should implement a second bridge code. Fortunately, with templates this won't be that hard.

Here is the code (godbold):

#include <iostream>

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 CommonInterface {
    public:
        virtual void doSomething() = 0;
};

template<class Child>
class ChildCaller : public CommonInterface {
    private:
        Child child;
    public:
        virtual void doSomething() {
            child.doSomething_Impl();
        }
};

class SomeLogicClass
{
    Type mClassId{ Type::base };
    ChildCaller<Child1> mChild1;
    ChildCaller<Child2> mChild2;
    ChildCaller<Child3> mChild3;

    CommonInterface* currentChild;

    public:
    Type getId() const { return mClassId; }
    void setId(Type id) { 
        mClassId = id; 
        switch(id) {
            case Type::child1:
                currentChild = &mChild1;
                break;
            case Type::child2:
                currentChild = &mChild2;
                break;
            case Type::child3:
                currentChild = &mChild3;
                break;
            default:
                currentChild = nullptr; // or anything you want
        }
    } 

    void doSomething() {
        currentChild->doSomething();
    }
};

void test(SomeLogicClass& ob, Type id)
{
    ob.setId(id);
    ob.doSomething(); // calls the corresponding implementations!
}

int main() {
    SomeLogicClass ob;    
    test(ob, Type::child1);
    test(ob, Type::child2);
    test(ob, Type::child3);
}

I omitted error checking here and there, so the code is more readable.

Werby answered 21/8, 2022 at 9:38 Comment(0)
B
1

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().

Blenheim answered 14/8, 2022 at 21:10 Comment(2)
@Blackthorn If you only have one active element (and you can destruct the non-active element), then it's essentially std::variant<>. Suggest using that directly.Blenheim
@Blackthorn If c++11, then you have boost::variant<>, which is 'essentially the same' (read: many nuance differences when you port, but likely works).Blenheim

© 2022 - 2024 — McMap. All rights reserved.