Search code examples
c++templatesc++17type-erasure

How to return one of two possible templated object types in C++?


I have created a minimal example based on my complicated project that is using boost:

template<typename T>
struct Data {
    T attribute;

    void print() {}
};

typedef Data<int> IntDataType;
typedef Data<float> FloatDataType;

std::shared_ptr<IntDataType> make_int_data() {
    return std::make_shared<IntDataType>();
}

std::shared_ptr<FloatDataType> make_float_data() {
    return std::make_shared<FloatDataType>();
}

std::any data;

void create_data(bool useFloat) {
    if (useFloat)
        data = make_float_data();
    else
        data = make_int_data();
}

auto get_data() {
    try {
        auto c = std::any_cast<FloatDataType>(data);
        return c;
    }
    catch (std::bad_any_cast const& ex) {
        std::cout << ex.what() << '\n';
    }

    auto c = std::any_cast<IntDataType>(data);
    return c;
}

int main() {
    create_data(true);
    get_data().print();
    return 0;
}

I have Data type that is based on a templated argument and I am created two Data types based on that: IntDataType and FloatDataType. I have a global variable std::any that could contain both of my types and sure I could try to cast to one type and to another.

The problem is that I am using these casts in a lot of places and the code is a mess. And as a result, I want to create a function that could return any of these types and make a simple call of method that is common for both (int our case print()). In my case this is a get_data() but it gives an error:

inconsistent deduction for auto return type: Data<float> and then Data<int>

I could not change the types because they are part of the boost library in my case. Is there a way to do it using modern C++? Will appreciate any help.


Solution

  • std::variant<std::shared_ptr<IntDataType>, std::shared_ptr<FloatDataType>> data;
    void create_data(bool useFloat) {
        if (useFloat)
            data = make_float_data();
        else
            data = make_int_data();
    }
    
    std::visit([](auto& elem) { elem->print(); }, data);
    

    std::variant can be used in more complex cases, I wrote about it once in my blog:

    Design Patterns: RunTime Reflection – C++

    Edit: An easier approach (as @PepijnKramer mentioned that is possible in that case) would be a common non-templated interface. Notice that it won't always be the case, and std::variant would usually be the easiest solution.

    struct DataI {
        virtual ~DataI() = default;
        virtual void print() {};
    };
    
    template<typename T>
    struct Data : DataI {
        T attribute;
    };
    
    std::shared_ptr<DataI> data;
    
    void create_data(bool useFloat) {
        if (useFloat)
            data = make_float_data();
        else
            data = make_int_data();
    }
    

    Another approach, in case that you don't want to use virtual tables, is a common template base class collection, introduced by Sean Parent. I also wrote about it:

    Template Base Class Collection