Arun
Arun

Reputation: 3408

Express constraints when using C++ templates

I have a Wrapper class. Any T or objects derived from T should be convertible to this Wrapper.

I would also like any object of 'Someclass' or objects derived from 'SomeClass' to be convertible to Wrapper. The implementation in both cases needs to be separate. How can I achieve this?

The following is the behavior I would want:

class SomeClass;

template <typename T>
class Wrapper
{
public:

    Wrapper(const T & val)
    {

    }

    template <typename E>
    Wrapper(const E & val)
    {
        static_assert(std::is_base_of<T, E>::value,"Wrapped object needs to be of type T or a type derived from T");

        // Some implementation
    }
    // Is it possible to combine the above 2 constructors into a single 
    // one? That would help too...

    // Can't use SomeClass directly as type because in case of derived
    // type of SomeClass, I want the exact type for downstream processing 
    // into another template call
    template <typename E> // ??
    Wrapper(const E & val)
    {
        static_assert(std::is_base_of<SomeClass, E>::value,"Wrapped object needs to be of type SomeClass or a type derived from SomeClass");

        // another implementation
    }
};

Not sure if I have been able to express my problem properly. Any help is greatly appreciated...

Upvotes: 0

Views: 104

Answers (1)

Let's start with why what you currently have doesn't work. A template function (constructors too) are identified by their signature. That signature includes what the template arguments are (types, non-types, templates), their order, as well as the functions arguments and return type (a constructor doesn't have a return type, but that's irrelevant to what we are trying to accomplish). So what you tried to do, involved declaring the same consturctor twice! Before the definition is even considered, you have a duplicate declaration, which is of course not allowed.

So what can we do? We can add parameters to each constructor. And if it'll differentiate them, they can co-exist. But co-existing is not enough, we want overload resolution to treat them differently. We want the first templated c'tor chosen for classes derived from T. And the second one for classes derived from SomeClass. Can we do that? Yes we can. If we make the template parameter we will add depend on our conditions, and fail substitution when the condition isn't met, that overload will be removed from consideration. That's SFINAE!

So putting all of that into practice:

template <typename E, std::enable_if_t<!std::is_same<SomeClass,T>::value &&
                                        std::is_convertible<E*, T*>::value>* = nullptr>
Wrapper(const E & val)
{
}

template <typename E, std::enable_if_t<std::is_convertible<E*, SomeClass*>::value>* = nullptr>
Wrapper(const E & val)
{

}

So what does the above do? It adds another template parameter, with a default argument. It does so conditionally, and if during substitution the condition isn't met, the signature of the "bad" c'tor is ill-formed. And it will not be considered in overload resolution.

I also took the liberty of adapting your condition. You probably want only classes that publicly derive from T and SomeClass to be accepted. That is something expressed better by std::is_convertible. std::is_base_of will allow private and ambiguous multiple inheritance too. I also made sure that in the case T is SomeClass we don't get two conflicting constructor declarations again.

Upvotes: 2

Related Questions