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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With