Static_Assert Dependent on Non-Type Template Parameter (Different Behavior on Gcc and Clang)

static_assert dependent on non-type template parameter (different behavior on gcc and clang)

Quotes found by @TartainLlama

If a hypothetical instantiation of a template immediately following its definition would be ill-formed due to a construct that does not depend on a template parameter, the program is ill-formed; no diagnostic is required.

N4296 [temp.res]/8

This applies immediately after the primary template is defined (the one with the static_assert in it). So the later specialization (for 42) cannot be considered, as it does not exist yet.

The next question is if static_assert( sizeof(answer) != sizeof(answer), depends on answer. Semantically it does not, syntactically it does, and standard-wise:

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

N4296 [temp.dep]/1

The construct sizeof(answer) != sizeof(answer) does not differ from one instantiation to another. So such a construct does not depend on the template parameters. Which means the entire static_assert does not depend on the template parameter.

Thus your program is ill formed, no diagnostic required. Issuing an arbitrary diagnostic (such as the static_assert failing) is valid compiler behavior. Missing the problem is valid compiler behavior. The behavior of a program compiled from an ill formed, no diagnostic required program is not defined by the standard: it is undefined behavior. Nasal demons are permitted.

Fancy attempts (like sizeof(int[answer])!=sizeof(int[answer]) may please the current god compiler, but does not make your program more well formed.

You could make a case where the compiler is unlikely to be able to catch you at it, but the ill-formed-ness remains regardless of the ability for the compiler to catch you with it. As a general rule, C++ wants to leave itself (and its compilers) freedom to find invalid template code "earlier than instantiation"; this means that template code must produce possibly legal code.

It is possible you want something like =delete with a message attached.

Partial specialization of single type template parameter class template using non-type template parameter through a dependent type

As far as I can tell, the first snippet is ill-formed (and a diagnostic is required); compilers should reject the program because of the partial specialization (2).

[temp.deduct.type]/18 applies here:

If P has a form that contains <i>, and if the type of i differs
from the type of the corresponding template parameter of the template
named by the enclosing simple-template-id, deduction fails. [...]

The associated example in the Standard uses a function template, but is otherwise very similar.

So the template argument of the partial specialization (2) can never be deduced, and [temp.class.spec.match]/3 applies:

If the template arguments of a partial specialization cannot be
deduced because of the structure of its template-parameter-list and
the template-id, the program is ill-formed.


Interestingly, I couldn't find a compiler that diagnoses this issue, not even EDG in strict mode. We could speculate that most compiler writers consider the benefits of having a diagnostic here not to be worth the effort of implementing the checks. This could mean that we might see the requirement in the paragraph above change in the future from ill-formed to ill-formed, no diagnostic required. However, this is pure speculation. In any case, I don't see it ever changing to well-formed; I can't think of a valid use for a partial specialization that never matches.


The wording of [temp.deduct.type]/18 was clarified by the resolution of CWG2091.

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

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.

static_assert in not initialized template class with valid specialization

clang++ is right to reject your code, but g++ is not wrong to fail to catch the error; this is a "no diagnostic required" situation.

The Standard strictly defines expressions within a template as "type-dependent" and/or "value-dependent". Given template<int i>, i is value-dependent but not type-dependent.

[14.6.2.2/4]: Expressions of the following forms are never type-dependent (because the type of the expression cannot be dependent):

  • ...
  • sizeof unary-expression
  • ...

[14.6.2.3/2]: Expressions of the following form are value-dependent if the unary-expression or expression is type-dependent or the type-id is dependent:

  • sizeof unary-expression
  • ...

So sizeof(i) is not dependent.

Finally, 14.6/8 says:

If a hypothetical instantiation of a template immediately following its definition would be ill-formed due to a construct that does not depend on a template parameter, the program is ill-formed; no diagnostic is required.

Behaviour of friend function template returning deduced dependent type in class template

Which compiler has the correct behaviour? Or, is the code ill-formed NDR or undefined behaviour? (And, why?)

As @Sedenion points out in a comment, whilst touching upon the domain of CWG 2118 (we'll return to this further down) this program is by the current standard well-formed and GCC is correct to accept it, whereas Clang and MSVC are incorrect to reject it, as governed by [dcl.spec.auto.general]/12 and /13 [emphasis mine]:

/12 Return type deduction for a templated entity that is a function or function template with a placeholder in its declared type
occurs when the definition is instantiated even if the function body contains a return statement with a non-type-dependent operand.

/13 Redeclarations or specializations of a function or function template
with a declared return type that uses a placeholder type shall also
use that placeholder, not a deduced type
. Similarly, redeclarations or
specializations of a function or function template with a declared
return type that does not use a placeholder type shall not use a
placeholder.

[Example 6:

auto f();
auto f() { return 42; } // return type is int
auto f(); // OK
// ...
template <typename T> struct A {
friend T frf(T);
};
auto frf(int i) { return i; } // not a friend of A<int>

Particularly the "not a friend of A<int>" example of /13 highlights that the redeclaration shall use a placeholder type (frf(T), and not a deduced type (frf(int)), whereas otherwise that particular example would be valid.

/12, along with [temp.inst]/3 (below) covers the that return type deduction for the friend occurs only after the friend's primary template definition has been made available (formally: it's declaration but not definition has been instantiated) from the instantiation of the S<double> enclosing class template specialization.

The implicit instantiation of a class template specialization causes

  • (3.1) the implicit instantiation of the declarations, but not of the definitions, of the non-deleted class member functions, member
    classes, scoped member enumerations, static data members, member
    templates, and friends; and
  • [...]

However, for the purpose of determining whether an instantiated
redeclaration is valid according to [basic.def.odr] and [class.mem], a
declaration that corresponds to a definition in the template is
considered to be a definition.

[Example 4:

// ...

template<typename T> struct Friendly {
template<typename U> friend int f(U) { return sizeof(T); }
};
Friendly<char> fc;
Friendly<float> ff; // error: produces second definition of f(U)

— end example]



CWG 2118: Stateful metaprogramming via friend injection

As covered in detail e.g. in A foliage of folly, one can rely on the single first instantiation of a class template to control how the definition of a friend looks like. First here means essentially relying on meta-programming state to specify this definition.

In OP's example the first instantiation, S<double>, is used to set the definition of the primary template of the friend foo such that its deduced type will always deduce to double, for all specializations of the friend. If we ever (in the whole program) instantiate, implicitly or explicitly, a second instantiation of S (ignoring S being specialized to remove the friend), we run into ODR-violations and undefined behavior. This means that this kind of program is, in practice, essentially useless, as it would serve clients undefined behavior on a platter, but as covered in the article linked to above it can be used for utility classes to circumvent private access rules (whilst still being entirely well-formed) or other hacky mechanisms such as stateful metaprogramming (which typically runs into or beyond the grey area of well-formed).



Related Topics



Leave a reply



Submit