c++templatesc++14sfinaeclass-template

How to restrict function template arguments to be specializations of a specific class template


I have a structure like:

template <typename Arg1, typename Arg2>
class TemplateClass { ... };

template <typename TClass>
class UsesTemplateClass {
public:
    UsesTemplateClass( TClass& instance ) inst{instance} { ... }
private:
    TClass& inst;
};

template <typename TClass>
auto make_uses_template_class( TClass& instance ) {
    return UsesTemplateClass<TClass>{ instance };
}

The reason make_uses_template_class exists is that the function template can deduce the type, so the user client doesn't have to explicitly specify it. I realize that C++17 has CTAD to better solve this. This works just fine as long as the type passed into make_uses_template_class really is a specialization of TemplateClass, but if it's not, the result will be some error in UsesTemplateClass.

I would like to make sure that a make_template_class overload doesn't exist if TClass isn't a TemplateClass. Also, I would like the error message to be reasonable. I know there are several ways to do this, but I don't see a lot of consistent guidance of how to use enablers or static_asserts in this sort of situation.

For instance, regarding the class, I thought I could do something like:

template <typename TClass>
class UsesTemplateClass;  // declared but not defined

template <typename Arg1, typename Arg2>
class UsesTemplateClass<Arg1, Arg2> {
   // real definition
};

which would work (if you instantiated it with anything other than a TemplateClass, it would complain that UsesTemplateClass<SomeOtherType> doesn't exist). I'm not thrilled that I'd have to explicitly specify the arguments to TemplateClass in my specialization because in the general case, there could be several template arguments that are subject to change.

Alternatively, I had the idea of putting something like using template_class_tag = void in TemplateClass and then defining UsesTemplateClass as:

template <typename TClass,
          typename = typename TClass::template_class_tag >
class UsesTemplateClass { ... };

but I see in several threads that using this sort of enabler for classes is generally frowned upon, and static_assert is generally recommended instead. I understand that the general consensus is that the static_assert could give a better error message and that it's not subject to misuse like a user specifying a type for the default template argument. Unfortunately, I don't believe it's possible to write a static assertion for whether the type TClass::template_class_tag exists.

To work around that problem, I thought I could give TemplateClass a non-template base and use a static assertion with std::is_base_of. I think that would work, though it's a bit intrusive (the base class would serve no other purpose).

Is there a generally accepted idiom for restricting a class like UsesTemplateClass in this way?

The function has the same issue, but I know that enablers and such are often used differently in functions than in classes, so I wanted to ask about that as well.


Solution

  • As "R Sahu" already pointed out in his "Approach 1" code example, not sure why "TClass" is any arbitrary type if only specializations of "TemplateClass" are allowed. Why not follow his basic "Approach 1" or similar. If "TClass" must be any arbitrary type though (for whatever reason), then the following code can be used as a more generic alternative to his "Approach 2" code example (TBH I didn't read his code in detail but the following is a generic technique you can use for any template taking type-based template args only - see "IsSpecialization" comments in code below - click here to run it):

    #include <type_traits>
    
    /////////////////////////////////////////////////////////////////////////////
    // IsSpecialization. Primary template. See partial specialization just below
    // for details.
    /////////////////////////////////////////////////////////////////////////////
    template <typename,
              template<typename...> class>
    struct IsSpecialization : public std::false_type
    {
    };
    
    /////////////////////////////////////////////////////////////////////////////
    // Partial specialization of (primary) template just above. The following
    // kicks in when the 1st template arg in the primary template above (a type)
    // is a specialization of the 2nd template arg (a template). IOW, this partial
    // specialization kicks in if the 1st template arg (a type) is a type created
    // from a template given by the 2nd template arg (a template). If not then the
    // primary template kicks in above instead (i.e., when the 1st template arg (a
    // type) isn't a type created from the template given by the 2nd template arg
    // (a template), meaning it's not a specialization of that template. Note that
    // "IsSpecialization" can handle templates taking type-based template args only
    // (handling non-type args as well is very difficult if not impossible in current
    // versions of C++)
    //
    //    Example
    //    -------
    //    template <class T>
    //    class Whatever
    //    {
    //       // Make sure type "T" is a "std::vector" instance
    //       static_assert(IsSpecialization<T, std::vector>::value,
    //                     "Invalid template arg T. Must be a \"std::vector\"");
    //    };
    //
    //    Whatever<std::vector<int>> whatever1; // "static_assert" above succeeds ("T" is a "std::vector")
    //    Whatever<std::list<int>> whatever2; // "static_assert" above fails ("T" is *not* a "std::vector")
    /////////////////////////////////////////////////////////////////////////////
    template <template<typename...> class Template,
              typename... TemplateArgs>
    struct IsSpecialization<Template<TemplateArgs...>, Template> : public std::true_type
    {
    };
    
    template <typename Arg1, typename Arg2>
    class TemplateClass
    {
    };
    
    template <typename TClass>
    class UsesTemplateClass
    {
        /////////////////////////////////////////////////////////////////
        // You can even create a wrapper for this particular call to
        // "IsSpecialization" that specifically targets "TemplateClass"
        // if you wish (to shorten the syntax a bit but I leave that to
        // you as an exercise). Note that in C++17 or later you should
        // also create the usual "IsSpecialization_v" helper variable
        // for "IsSpecialization" (can also be done in C++14 but "_v"
        // variables in <type_traits> itself is a C++17 feature and
        // they're declared "inline" which is also a C++17 feature, so
        // such variables in your own code is more consistent with C++17
        // IMHO and therefore less confusing), and in C++20 or later a
        // "concept" for it as well (all this getting off-topic though).
        /////////////////////////////////////////////////////////////////
        static_assert(IsSpecialization<TClass, TemplateClass>::value,
                      "Invalid template arg \"TClass\". Must be a \"TemplateClass\" specialization");
    public:
        UsesTemplateClass(TClass &instance)
            : inst{instance}
        {
            // ...
        }
    private:
        TClass& inst;
    };
    
    template <typename TClass>
    auto make_uses_template_class( TClass& instance )
    {
        return UsesTemplateClass<TClass>{ instance };
    }
    
    int main()
    {
        // Compiles ok ("tc" is a "TemplateClass" specialization)
        TemplateClass<int, double> tc;
        auto utc1 = make_uses_template_class(tc);
        UsesTemplateClass<decltype(tc)> utc2(tc);
    
        // Triggers "static_assert" above ("i" is not a "TemplateClass" specialization)
        int i;
        auto utc3 = make_uses_template_class(i);
        UsesTemplateClass<decltype(i)> utc4(i);
    
        return 0;
    }