Can I associate one class with another from a template (using C++17 variant)?

Viewed 163

I have some code that accepts one type of object and creates another type of object based on the type of the first. (There is a 1->1 relationship between the types.) I originally used a hash table (unordered_map<>) with a key based on the type of the first object to associate a creation function for the second object. But as I am learning more about the C++ features introduced since the last time I was full-time C++, I discovered std::variant<>.

I have successfully converted the implementation to use this C++17 feature. However, there is one remaining piece that is still a bit cumbersome. The design makes a call to a static member function of the second class to validate the contents of the first object, before instantiating an object of the second class. To handle this right now, I'm using a visitor structure with function operators overloaded for each input type.

What I'm wondering is if there is some way to use a template for the association, rather than the copied code with only the types different?

I've tried looking at the way std::variant<> works, and I see where the index of the type can be obtained with .index(). I can see how to instantiate an object based on an index, which I might use if I created a second std::variant<> with the object types. But, as you can see, I don't want to instantiate the object until the parameters have been validated. The function that does that is static, and I don't see a way to associate the parms type with the object type in a way that lets me make the static call.

(I also realize that these two visitor structures can be combined in the code below, but in the real code, the creation is longer and more complicated, and I would rather not have copies of it in each overload.)

struct Type1Parms {};
struct Type2Parms {};
struct Type3Parms {};
...

struct TypeBase {};
struct Type1 : public TypeBase
{
    static bool ValidateParms(const Type1Parms&);
    Type1(const Type1Parms&);
};
struct Type2 : public TypeBase
{
    static bool ValidateParms(const Type2Parms&);
    Type2(const Type2Parms&);
};
struct Type3 : public TypeBase
{
    static bool ValidateParms(const Type3Parms&);
    Type3(const Type3Parms&);
};
...

struct ValidateParmsVisitor
{
    bool operator()(const Type1Parms& parms)
    {
        return Type1::ValidateParms(parms);
    }
    bool operator()(const Type2Parms& parms)
    {
        return Type2::ValidateParms(parms);
    }
    bool operator()(const Type3Parms& parms)
    {
        return Type3::ValidateParms(parms);
    }
...
};

using TypeParms = std::variant<Type1Parms, Type2Parms, Type3Parms, ...>;

struct CreateObjectVisitor
{
    std::unique_ptr<TypeBase> operator()(const Type1Parms& parms)
    {
        return std::make_unique<Type1>(parms);
    }
    std::unique_ptr<TypeBase> operator()(const Type2Parms& parms)
    {
        return std::make_unique<Type2>(parms);
    }
    std::unique_ptr<TypeBase> operator()(const Type3Parms& parms)
    {
        return std::make_unique<Type3>(parms);
    }
...
};

template<typename TParms>
std::unique_ptr<TypeBase> CreateType(const TParms& parms)
{
    unique_ptr<TypeBase> obj;
    if (visit(ValidateParmsVisitor{}, parms))
        obj = visit(CreateObjectVisitor{}, parms);
    return std::move(obj);
}

Is there a way to make this association, especially as a type that can be used with a static member function call?

EDIT: I should explain that this is part of a much larger project, with a number of other design criteria that shape its design.

For example, this is for a client interface, where the API is meant to be as simple as can be expressed. The client only has visibility (via header) to the parms structures and a function that takes the parms & returns an object that contains the objects mentioned above. The original design did indeed have a base structure for the parms, which obviously had to be in the public header. However, this meant that a client could inherit from the base class themselves and pass this into the object creation function, or inherit from the acceptable structures. To avoid segfaults, this necessitated adding runtime checks to be sure the types were acceptable, which was mostly handled by the hash design--although it wasn't quite that simple. When I removed the hash design, I also lost this method of type validation, but I recognized that this would be replaced by a compile time check with the variant<>, handling custom structures (no base to check now). I also learned about the C++ version of the final keyword which handled the inheritance issue.

Additionally, while the code above does not show it, the parms structures contain multiple members and the ValidateParms() functions actually attempt to validate whether the values and combinations are valid.

2 Answers

You can create traits for the association:

template <typename T> struct from_param;

template <> struct from_param<Type1Parms> { using type = Type1; };
template <> struct from_param<Type2Parms> { using type = Type2; };
template <> struct from_param<Type3Parms> { using type = Type3; };

Then, you might do

using TypeParms = std::variant<Type1Parms, Type2Parms, Type3Parms>;

std::unique_ptr<TypeBase> CreateType(const TypeParms& parms)
{
    if (std::visit([](const auto& param){
            return from_param<std::decay_t<decltype(param)>>::type::ValidateParms(parms);
        }, parms))
    {
        return std::visit([](const auto& param) -> std::unique_ptr<TypeBase> {
            return std::make_unique<typename from_param<std::decay_t<decltype(param)>>::type>(parms);
        }, parms);
    }
    return nullptr;
}

Demo

or without variant, if you call with correct type:

template <typename T>
auto CreateType(const T& parms)
{
    if (from_param<T>::type::ValidateParms(parms))
    {
        return std::make_unique<typename from_param<T>::type>(parms);
    }
    return nullptr;
}

There is a very simple method, a set of overloaded functions:

unique_ptr<TypeBase> CreateType(Type1Params const& params)
{
    return make_unique<Type1>(params);
}
unique_ptr<TypeBase> CreateType(Type2Params const& params)
{
    return make_unique<Type2>(params);
}
unique_ptr<TypeBase> CreateType(Type3Params const& params)
{
    return make_unique<Type3>(params);
}

Notes:

  • You can add another overload to catch other parameters and then return null, but I think a compile-time error would be preferable.
  • You could also use a template function and specializations, but there's probably little typing to safe that way.
Related