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

192 views Asked by At

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.

3

There are 3 answers

11
Larry On BEST ANSWER

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;
}
2
R Sahu On

What I would like to do is make sure that a make_template_class overload doesn't exist if TClass isn't a TemplateClass. Also, if someone manually tries to instantiate a UsesTemplateClass with an argument that isn't a TemplateClass, I would like the error message to be reasonable.

Approach 1

A simple resolution to that would be to change the template definitions to:

template <typename T1, typename T2>
class UsesTemplateClass {
public:
    UsesTemplateClass( TemplateClass<T1, T2>& instance ) : inst{instance} {}
private:
    TemplateClass<T1, T2>& inst;
};

template <typename T1, typename T2>
auto make_uses_template_class( TemplateClass<T1, T2>& instance ) {
    return UsesTemplateClass<T1, T2>{ instance };
}

With that change, the following will work:

TemplateClass<int, double> t;
auto a = make_uses_template_class(t);
auto b = UsesTemplateClass<int, double>(t);

but the following will result in compiler error:

int i = 0;
auto a = make_uses_template_class(i);
auto b = UsesTemplateClass<int>(i);

Approach 2

You can use type traits-like logic to enforce your requirement as well.

Add the following as supporting code after TemplateClass

struct is_template_class_imp
{
    struct __two {char _; char __;};
    template <class t> static __two __test(t* ptr);
    template <class t1, class t2> static char __test(TemplateClass<t1, t2>* ptr);
};

template <typename T>
struct is_template_class : std::integral_constant<bool, sizeof(is_template_class_imp::__test((T*)0)) == 1> {};

Update UsesTemplateClass to use static_assert.

template <typename TClass>
class UsesTemplateClass {
public:
    static_assert(is_template_class<TClass>::value, "Template parameter needs to be a TemplatClass");
    UsesTemplateClass( TClass& instance ) : inst{instance} {}
private:
    TClass& inst;
};

With those updates, the following will work:

TemplateClass<int, double> t;
auto a = make_uses_template_class(t);
auto b = UsesTemplateClass<int, double>(t);

but the following will result in compiler error:

int i = 0;
auto a = make_uses_template_class(i);
auto b = UsesTemplateClass<int>(i);

Update

Jan Schultke provided a cleaner implementation of is_template_class at https://godbolt.org/z/d68WrzTcP.

Here's the code from that link:

template <typename T>
struct is_template_class
{
    static std::false_type test(...);
    template <typename A, typename B>
    static std::true_type test(TemplateClass<A, B>*);

    static constexpr bool value = decltype(test(static_cast<T*>(nullptr)))::value;
};

static_assert(is_template_class<TemplateClass<int, float>>::value, "");
static_assert(!is_template_class<int>::value, "");
11
Ted Lyngmo On

You could add a type trait:

#include <type_traits>

template <class...>         // primary
struct is_TemplateClass : std::false_type {};

template <class A, class B> // specialization
struct is_TemplateClass<TemplateClass<A, B>> : std::true_type {};

template <class T>          // helper variable template
static constexpr bool is_TemplateClass_v = is_TemplateClass<T>::value;

And you could use it in a static_assert to make the error message clear:

template <typename TClass>
auto make_uses_template_class(TClass& instance) {
    static_assert(
        is_TemplateClass_v<std::remove_cv_t<std::remove_reference_t<TClass>>>,
        "Not a TemplateClass");
    return UsesTemplateClass<TClass>{instance};
}

Demo


Or using SFINAE if you want overloads for make_uses_template_class where TemplateClass-based types are not the template parameter:

template <typename TClass>
std::enable_if_t<
    is_TemplateClass_v<std::remove_cv_t<std::remove_reference_t<TClass>>>,
    UsesTemplateClass<TClass>>
make_uses_template_class(TClass& instance) {
    return UsesTemplateClass<TClass>{instance};
}