简体   繁体   中英

A safe, standard-compliant way to make a class template specialization fail to compile using `static_assert` only if it is instantiated?

Assume that we want to make a template class that can only be instantiated with numbers and should not compile otherwise. My attempt:

#include <type_traits>

template<typename T, typename = void>
struct OnlyNumbers{
public:
    struct C{};
    static_assert(std::is_same<C,T>::value, "T is not arithmetic type.");

    //OnlyNumbers<C>* ptr;
};

template<typename T>
struct OnlyNumbers<T, std::enable_if_t<std::is_arithmetic_v<T>>>{};

struct Foo{};
int main()
{
    OnlyNumbers<int>{}; //Compiles
    //OnlyNumbers<Foo>{}; //Error
}

Live demo - All three major compilers seem to work as expected. I'm aware that there is already a similar question with answers quoting the standard. The accepted answer uses temp.res.8 together with temp.dep.1 to answer that question. I think my question is a bit different because I'm asking precisely about my example and I'm not sure about the standard's opinion on it.

I would argue that my program is not ill-formed and that it should fail to compile if and only if the compiler tries to instantiate the base template. My reasoning:

  • [temp.dep.1]:

    Inside a template, some constructs have semantics which may differ from one instantiation to another. Such a construct depends on the template parameters.

    This should make std::is_same<C,T>::value dependent on T .

  • [temp.res.8.1]:

    no valid specialization can be generated for a template or a substatement of a constexpr if statement within a template and the template is not instantiated, or

    Does not apply because there exist a valid specialization, in particular OnlyNumbers<C> is valid and can be used inside the class for eg defining a member pointer variable( ptr ). Indeed by removing the assert and uncommenting the ptr , OnlyNumbers<Foo> lines the code compiles.

  • [temp.res.8.2 - 8.4] does not apply.

  • [temp.res.8.5] I don't think this applies either but I cannot say that I fully understand this section.

My question is: Is my reasoning correct? Is this a safe, standard-compliant way to make a particular [class]* template fail to compile using static_assert if** and only if it is instantiated?

*Primarily I'm interested in class templates, feel free to include function templates. But I think the rules are the same.

**That means that there is no T which can be used to instantiate the template from the outside like T=C could be used from the inside. Even if C could be accessed somehow I don't think there's a way to refer to it because it leads to this recursion OnlyNumbers<OnlyNumbers<...>::C> .

EDIT:

Just to clarify, I know that I can construct an expression that will be false exactly if none of the other specializations match. But that can become wordy quite quickly and is error-prone if specializations change.

Static assertions are there to be used directly in the class without doing anything complicated.

#include <type_traits>

template<typename T>
struct OnlyNumbers {
    static_assert(std::is_arithmetic_v<T>, "T is not arithmetic type.");
    // ....
};

In some cases, you might get additional error messages since instanciating OnlyNumbers for non-arithmetic types might cause more compilation errors.

One trick I have used from time to time is

#include <type_traits>

template<typename T>
struct OnlyNumbers {
    static_assert(std::is_arithmetic_v<T>, "T is not arithmetic type.");
    using TT = std::conditional_t<std::is_arithmetic_v<T>,T,int>;
    // ....
};

In this case, your class gets instanciated with int, a valid type. Since the static assertion fails anyway, this does not have negative effects.

Well... I don't understand what do you mean with

[[temp.res.8.1]] Does not apply because there exist a valid specialization, in particular OnlyNumbers is valid and can be used inside the class for eg defining a member pointer variable(ptr).

Can you give an example of OnlyNumers valid and compiling main template based on OnlyNumbers<C> ?

Anyway, it seems to me that the point is exactly this.

If you ask

Is this a safe, standard-compliant way to make a particular [class]* template fail to compile using static_assert if** and only if it is instantiated?

it seems to me that (maybe excluding a test that is true only when another specialization matches) the answer is "no" because of [temp.res.8.1].

Maybe you could let a little open door open to permit an instantiation but available only is someone really (really!) want instantiate it.

By example, you could add a third template parameter, with different default value, and something as follows

template<typename T, typename U = void, typename V = int>
struct OnlyNumbers
 {
   static_assert(std::is_same<T, U>::value, "test 1");
   static_assert(std::is_same<T, V>::value, "test 2");
 };

This way you open a door to a legit instantiation

OnlyNumbers<Foo, Foo, Foo>     o1;
OnlyNumbers<void, void, void>  o2;
OnlyNumbers<int, int>          o3;

but only explicating at least a second template type.

Anyway, why don't you simply avoid to define the main version of the template?

// declared but (main version) not defined
template<typename T, typename = void>
struct OnlyNumbers;

// only specialization defined
template<typename T>
struct OnlyNumbers<T, std::enable_if_t<std::is_arithmetic_v<T>>>
 { };

Your code is ill-formed since the primary template cannot be instantiated. See the standard quote in Barry's answer to the related question you linked to. The roundabout way you have used to ensure that the clearly stated standard requirement cannot be met, does not help. Stop fighting your compiler rsp. the standard, and go with Handy999's approach. If you still don't want to do that eg for DRY reasons, then a conformant way to achieve your goal would be:

template<typename T, typename Dummy = void>
struct OnlyNumbers{
public:
    struct C{};
    static_assert(! std::is_same<Dummy, void>::value, "T is not a number type.");

Two remarks:

  • First, I deliberately replaced the error message because the error message "is not an arithmetic type" screams that you must test ! std::is_arithmetic<T>::value ! std::is_arithmetic<T>::value . The approach I've outlined potentially makes sense if you have multiple overloads for "numeric" types, some of which meet the standard's arithmetic type requirements and others might not (eg maybe a type from a multiprecision library).
  • Second, you might object that someone could write eg OnlyNumbers<std::string, int> to defeat the static assertion. To which I say, that's their problem. Remember that every time you make something idiot proof, nature makes a better idiot. ;-) Seriously, do make APIs that are easy to use and hard to abuse, but you cannot fix insanity and shouldn't bother trying.

TL;DR: KISS and SWYM (say what you mean)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM