7

I read a couple of answers yesterday, on the use of static_assert(false, "Some message") inside the else clause of an if constexpr. I understand that it's considered to be ill-formed, according to the standard (even if some compilers, including MSVC2017, will accept it). Qt will also mark this as an error.

My question is, is the code below well-formed according to the standard? (I'm inclined to think so, but I'd like a confirmation.)

template <typename TypeOfValue>
static void PushValue(duk_context* ctx, TypeOfValue value) {
    // Push value onto duktape stack
    if constexpr (std::is_same<TypeOfValue, int>::value) {
        // Push int
        duk_push_int(ctx, value);
    } else if constexpr (std::is_same<TypeOfValue, uint32_t>::value) {
        // Push uint
        duk_push_uint(ctx, value);
    } else {
        // Unsupported type
        static_assert(bool_value<false, TypeOfValue>(), "Unsupported type");
    }    
}

template <bool value, typename T>
static constexpr bool bool_value() {return value;}        

Edit:

It seems, from the comment I got, that bool_value should instead be defined like this:

template<bool value, typename T>
struct bool_value { 
    static constexpr bool value = value; 
};

with the usage pattern

// Unsupported type
static_assert(bool_value<false, TypeOfValue>::value, "Unsupported type");

It's then well-formed, only because it's possible for bool_value to be specialized into a version which returns true for the expression bool_value<false, TypeOfValue>::value.

curiousguy
  • 8,038
  • 2
  • 40
  • 58
bjaastad_e
  • 691
  • 6
  • 10
  • [cppreference reads](https://en.cppreference.com/w/cpp/language/if#Constexpr_If): "the discarded statement can't be ill-formed for every possible specialization". But `bool_value()` is `false` for every possible specialization. – Evg Jun 13 '20 at 11:57
  • 2
    I remember a discussion after one of C++ conference presentations (I don't remember which one, sorry) about such code. The outcome was something like this: practically, this always works, but theoretically, this is still ill-formed, because `bool_value::value` is always `false` unless you actually have a specialization that returns `true` (e.g. `std::is_void_t`). Consider adding the [tag:language-lawyer] tag to your question. – Evg Jun 13 '20 at 14:57
  • If i get what you're trying to do, i would elevate the check into the immediate context of the template. I can conjure up a godbolt if you want. – Taekahn Jun 16 '20 at 15:34
  • @Taekahn The goal is to get a compiler error if I use the template with the wrong parameters. I know it can be solved in other ways (separate overloaded functions, for instance), but I'm looking for a judgement on this particular way of doing it. – bjaastad_e Jun 17 '20 at 07:26
  • @Elling I would just use the exact example from cppreference then. It's a little bit cleaner. – Taekahn Jun 17 '20 at 12:35

3 Answers3

5

Both of your attempts (with the function and with the struct) are well-formed as is.

The other answer mentions [temp.res]/8, but I disagree with how it was interpreted.

The validity of a template may be checked prior to any instantiation. ... 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 within a template and the template is not instantiated, or ...

Both the function and struct you wrote can be specialized to be true. I believe the mere possibility of specialization is enough, you don't actually need to add a dummy true specialization to make the code well-formed.

According to my understanding (and according to common sense, I hope), the point of this part of the standard is to allow compilers to check validity of templates and if constexpr branches early (when they are seen for the first time), and reject the ones that can't possibly be instantiated.

Every branch in your template can potentially be instantiated, because bool_value() can be specialized later. I doubt a sane compiler is going to reject your code due to bool_value() not being specialized yet.

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
1

The accepted answer of the question you linked explains why it's ill-formed. Note the quote from the Standard temp.res-8, which says (emphasis mine)

The validity of a template may be checked prior to any instantiation. [...] 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 within a template and the template is not instantiated, or...

[...]

Otherwise, no diagnostic shall be issued for a template for which a valid specialization can be generated. [ Note: If a template is instantiated, errors will be diagnosed according to the other rules in this document. Exactly when these errors are diagnosed is a quality of implementation issue. — end note ]

In the cppreference page about if you can see the following workaround (emphasis mine)

The common workaround for such a catch-all statement is a type-dependent expression that is always false:

template<class T> struct dependent_false : std::false_type {};
template <typename T>
void f() {
     if constexpr (std::is_arithmetic_v<T>)
         // ...
     else
       static_assert(dependent_false<T>::value, "Must be arithmetic"); // ok
}

As noted in the comments by Evg

theoretically, this is still ill-formed, because [...] is always false unless you actually have a specialization that returns true.

So you could add that specialization to the previous snippet

namespace {
    // Do not unleash.
    struct aggressive_unicorn_type;
}

template<> struct dependent_false<aggressive_unicorn_type> : std::true_type {};

Whether this is necessary to satisfy the Standard document to the letter or not it depends on the interpretation of "can be generated".

I wonder if it really matters too. The OP is asking if the code is well-formed, but they are actually trying to produce a code which is ill-formed, with a diagnostic (that's what static_assert(false) implies, see dcl.pre) under certain conditions.

As the asker already noted, there are other methods to force a compiler error when arguments of the "wrong" type are passed (consider concepts, of the upcoming standard) and, maybe, if we want to use static_asserts in conjunction with if constexpr the best option may be a type-dependent expression that is not always false.

#include <type_traits>

void f(int) {};
void g(unsigned) {};

template< class T>
constexpr inline bool is_supported =
    std::is_same_v<T, int> ||
    std::is_same_v<T, unsigned> ||
    std::is_same_v<T, char>;

template <class T>
void use(T value) {
    static_assert(is_supported<T>, "Not supported");
    if constexpr (std::is_same<T, int>::value) {
        f(value);
    } else if constexpr (std::is_same_v<T, unsigned>){
        g(value);
    } else {
        static_assert( !is_supported<T>, "Not yet implemented");
    }
}
Bob__
  • 12,361
  • 3
  • 28
  • 42
1

I'll just add my own final thoughts as an answer, since I seem to not be getting a quite principled enough answer from other sources.

My conclusion after reading a couple of other answers is this:

  1. The compiler is allowed, according to the standard, to evaluate static_assert(false, "...") early. If it does, it produces a program that can never compile.

  2. A program that can never compile is ill-formed.

So, the problem is that the compiler is allowed to evaluate static_assert early, even within an if constexpr.

Forcing it to delay the evaluation until instantiation time, by introducing a false that artificially depends upon a template parameter, seems a bit contrived to me. But I accept that there are other considerations which probably makes it the correct language choice anyway. (That's the sort of thing I was hoping for an expert answer to shed some light upon.)

bjaastad_e
  • 691
  • 6
  • 10