constexpr if and static_assert

2019-01-09 08:26发布

问题:

P0292R1 constexpr if has been included, on track for C++17. It seems useful (and can replace use of SFINAE), but a comment regarding static_assert being ill-formed, no diagnostic required in the false branch scares me:

Disarming static_assert declarations in the non-taken branch of a
constexpr if is not proposed.

void f() {
  if constexpr (false)
    static_assert(false);   // ill-formed
}

template<class T>
void g() {
  if constexpr (false)
    static_assert(false);   // ill-formed; no 
               // diagnostic required for template definition
}

I take it that it's completely forbidden to use static_assert inside constexpr if (at least the false / non-taken branch, but that in practice means it's not a safe or useful thing to do).

How does this come about from the standard text? I find no mentioning of static_assert in the proposal wording, and C++14 constexpr functions do allow static_assert (details at cppreference: constexpr).

Is it hiding in this new sentence (after 6.4.1) ? :

When a constexpr if statement appears in a templated entity, during an instantiation of the enclosing template or generic lambda, a discarded statement is not instantiated.

From there on, I assume that it is also forbidden, no diagnostic required, to call other constexpr (template) functions which somewhere down the call graph may call static_assert.

Bottom line:

If my understanding is correct, doesn't that put a quite hard limit on the safety and usefulness of constexpr if as we would have to know (from documentation or code inspection) about any use of static_assert? Are my worries misplaced?

Update:

This code compiles without warning (clang head 3.9.0) but is to my understanding ill-formed, no diagnostic required. Valid or not?

template< typename T>
constexpr void other_library_foo(){
    static_assert(std::is_same<T,int>::value);
}

template<class T>
void g() {
  if constexpr (false)
    other_library_foo<T>(); 
}

int main(){
    g<float>();
    g<int>();
}

回答1:

This is talking about a well-established rule for templates - the same rule that allows compilers to diagnose template<class> void f() { return 1; }. [temp.res]/8 with the new change bolded:

The program is ill-formed, no diagnostic required, if:

  • no valid specialization can be generated for a template or a substatement of a constexpr if statement ([stmt.if]) within a template and the template is not instantiated, or
  • [...]

No valid specialization can be generated for a template containing static_assert whose condition is nondependent and evaluates to false, so the program is ill-formed NDR.

static_asserts with a dependent condition that can evaluate to true for at least one type are not affected.



回答2:

Edit: I'm keeping this self-answer with examples and more detailed explanations of the misunderstandings that lead to this questions. The short answer by T.C. is strictly enough.

After rereading the proposal and on static_assert in the current draft, and I conclude that my worries were misguided. First of all, the emphasis here should be on template definition.

ill-formed; no diagnostic required for template definition

If a template is instantiated, any static_assert fire as expected. This presumably plays well with the statement I quoted:

... a discarded statement is not instantiated.

This is a bit vague to me, but I conclude that it means that templates occurring in the discarded statement will not be instantiated. Other code however must be syntactically valid. A static_assert(F), [where F is false, either literally or a constexpr value] inside a discarded if constexpr clause will thus still 'bite' when the template containing the static_assert is instantiated. Or (not required, at the mercy of the compiler) already at declaration if it's known to always be false.

Examples: (live demo)

#include <type_traits>

template< typename T>
constexpr void some_library_foo(){
    static_assert(std::is_same<T,int>::value);
}

template< typename T>
constexpr void other_library_bar(){
    static_assert(std::is_same<T,float>::value);
}

template< typename T>
constexpr void buzz(){
    // This template is ill-formated, (invalid) no diagnostic required,
    // since there are no T which could make it valid. (As also mentioned
    // in the answer by T.C.).
    // That also means that neither of these are required to fire, but
    // clang does (and very likely all compilers for similar cases), at
    // least when buzz is instantiated.
    static_assert(! std::is_same<T,T>::value);
    static_assert(false); // does fire already at declaration
                          // with latest version of clang
}

template<class T, bool IntCase>
void g() {
  if constexpr (IntCase){
    some_library_foo<T>();

    // Both two static asserts will fire even though within if constexpr:
    static_assert(!IntCase) ;  // ill-formated diagnostic required if 
                              // IntCase is true
    static_assert(IntCase) ; // ill-formated diagnostic required if 
                              // IntCase is false

    // However, don't do this:
    static_assert(false) ; // ill-formated, no diagnostic required, 
                           // for the same reasons as with buzz().

  } else {
    other_library_bar<T>();
  }      
}

int main(){
    g<int,true>();
    g<float,false>();

    //g<int,false>(); // ill-formated, diagnostic required
    //g<float,true>(); // ill-formated, diagnostic required
}

The standard text on static_assert is remarkably short. In standardese, it's a way to make the program ill-formed with diagnostic (as @immibis also pointed out):

7.6 ... If the value of the expression when so converted is true, the declaration has no effect. Otherwise, the program is ill-formed, and the resulting diagnostic message (1.4) shall include the text of the string-literal, if one is supplied ...