Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 e.g. 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.

like image 570
Quimby Avatar asked Jun 16 '19 12:06

Quimby


1 Answers

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.

like image 122
Handy999 Avatar answered Sep 25 '22 01:09

Handy999