What's the right way to call static_assert(false)?
Asked Answered
N

5

11

I’m trying to use static_assert to force something to fail. If you try to instantiate a specific templated function in a specific way I want to generate a complier error. I could make it work, but it was really ugly. Is there an easier way to do this?

This was my first attempt. This did not work at all. It always generates an error, even if no one tries to use this function.

template< class T >
void marshal(std::string name, T  *value)
{
  static_assert(false, "You cannot marshal a pointer.");
}

Here’s my second attempt. It actually works. If you don’t call this, you get no error. If you do call this, you get a very readable error message that points to this line and points to the code that tried to instantiate it.

template< class T >
void marshal(std::string name, T  *value)
{
  static_assert(std::is_pod<T>::value && !std::is_pod<T>::value, "You cannot marshal a pointer.");
}

The problem is that this code is ugly at best. It looks like a hack. I’m afraid the next time I change the optimization level, upgrade my compiler, sneeze, etc, the compiler will realize that this second case is the same as the first, and they will both stop working.

Is there a better way to do what I’m trying to do?

Here’s some context. I want to have several different versions of marshal() which work for different input types. I want one version that uses a template as the default case. I want another one that specifically disallows any pointers except char *.

void marshal(std::string name, std::string)
{ 
  std::cout<<name<<" is a std::string type."<<std::endl;
}

void marshal(std::string name, char *string)
{
  marshal(name, std::string(string));
}

void marshal(std::string name, char const *string)
{
  marshal(name, std::string(string));
}

template< class T >
void marshal(std::string name, T value)
{
  typedef typename std::enable_if<std::is_pod<T>::value>::type OnlyAllowPOD;
  std::cout<<name<<" is a POD type."<<std::endl;
}

template< class T >
void marshal(std::string name, T  *value)
{
  static_assert(false, "You cannot marshal a pointer.");
}

int main (int argc, char **argv)
{
  marshal(“should be pod”, argc);
  marshal(“should fail to compile”, argv);
  marshal(“should fail to compile”, &argc);
  marshal(“should be std::string”, argv[0]);
}
Nara answered 19/5, 2017 at 0:14 Comment(5)
Why typedef typename std::enable_if?Sedum
@Sedum Some people hide enable_if in an optional parameter to the the template. This seems better because the caller can't accidentally fill that (fake) parameter in.Nara
There is a proposal to allow static_assert(false), wg21.link/p2593 Maybe available in C++26?Recap
@Recap a Defect Report back to C++23, AFAICT: cplusplus.github.io/CWG/issues/2518.htmlConsistent
@Consistent - Ah, good! Sometimes it is hard to see what is a defect and what is a request for a new feature. :-)Recap
L
17

Prior to a DR against , there was no way to do this. You might be able to make it work on your compiler, but the resulting program is ill formed no diagnostic required.

Use =delete.

template< class T >
void marshal(std::string name, T  *value) = delete;

After 's DR 2518 was resolved, the wording was tweaked:

In a static_assert-declaration, the constant-expression is contextually converted to bool and the converted expression shall be a constant expression (7.7 [expr.const]). If the value of the expression when so converted is true or the expression is evaluated in the context of a template definition, the declaration has no effect. Otherwise, the static_assert-declaration fails, the program is ill-formed, and the resulting diagnostic message (4.1 [intro.compliance]) should include the text of the string-literal, if one is supplied.

the bolded text means that a static_assert(false, "whatever") in a template definition but not instantiation doesn't cause your program to be in violation of the C++ standard. In addition, clauses that require all templates to have a valid instantiation where relaxed to exclude failures caused by static_asserts.

The result is this example from the amended standard:

template <class T>
void f(T t) {
  if constexpr (sizeof(T) == sizeof(int)) {
    use(t);
  } else {
    static_assert(false, "must be int-sized");
  }
}

being perfectly a-ok, with the failure occurring if you call f with a non-int sized T.

Defect reports are considered retroactive changes - so a conforming compiler must implement the defect report, and can do so without having to say they are using a 'newer' version of the standard. Not all compilers will do so, both because most C++ compilers tend to be incomplete implementations of a standard, and because they may have been written before was redefined.

Lardon answered 19/5, 2017 at 8:53 Comment(2)
Interesting. I've only seen =delete used to remove default methods, like a copy constructor. I didn't even know you could do this. I'll have to give it a try.Nara
There is now, given this DR against C++23: cplusplus.github.io/CWG/issues/2518.htmlConsistent
E
5

Relying on a contradiction is not the best indeed, but there's a simpler way:

template <class...>
struct False : std::bool_constant<false> { };

template <class T>
void bang() {
    static_assert(False<T>{}, "bang!");
}

Why does this not fall under the "no valid specialization" case?
Well, because you can actually make a valid specialization, with that second half of the code:

template <>
struct False<int> : std::bool_constant<true> { };

int main() {
    bang<int>(); // No "bang"!
}

Of course, no one is actually going to specialize False to break your assertions in real code, but it is possible :)

Eviscerate answered 19/5, 2017 at 8:30 Comment(6)
If the compiler knows that there is no valid specialization (ie, that False is never specialized, because it has your source code), wouldn't it then prove your program is ill-formed no diagnostic required? Ie, are you sure this avoids that rule, or does it just make it harder to notice?Lardon
@Yakk I think that's stretching it. False can be specialized. That it be not in the current program is outside of the scope of that bullet point AFAICT. What if I dynamically load another binary that has specialized False, does that make my program well-formed again?Eviscerate
So, you aren't sure? I get the reasonableness of wanting to be able to static assert false, but the standard was written to say "no"; the exact wording of the standard matters a lot when you are skirting close to the edge. It states no arguments would make the specialization instantiatable; until False<?>:std::bool_constant<true>{} actually exists for some ? it it is true that no arguments would make the specialization instantiatable. I could be wrong; I often am. But I'd have an abundance of caution around "ill formed, no diagnostic required"Lardon
@Yakk What I'm not sure about is whether your jump from "can" to "shall" is justified. Would adding template <> struct False<struct gobbledigook> : std::bool_constant<true> {}; be a fair compromise? :)Eviscerate
Sure, so long as struct gobbledigook as T works for the rest of the body of the function. Which I doubt. The entire function's body must have some set of template arguments that make it possible to instantiate.Lardon
@Yakk well, in this particular case it does, since the body is otherwise empty. But fair point -- that's also the case where = delete; is a better choice.Eviscerate
U
3

What you are trying to do is doomed to be ill-formed (even your workaround can fail) according to [temp.res]/8 (emphasis mine):

Knowing which names are type names allows the syntax of every template to be checked. 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 (...)

Ulphi answered 19/5, 2017 at 7:42 Comment(2)
I read this 10 times last night. And again this morning. I still don't understand what it's saying. :(Nara
Unfortunately standards are written using very precise but sometimes hard to understand wording... Basically it says that you cannot create a template (and it doesn't matter if it is a struct template or a function template) that cannot be instantiated by any parameters... in other words there must be at least one parameters set for which template can be instantiatedUlphi
S
2

I don't understand why you have template< class T > void marshal(std::string name, T *value) in the first place. This should just be a static_assert in the primary template.

That is, you should change the definition of your primary template to

template< class T >
void marshal(std::string name, T value)
{
  static_assert(std::is_pod<T>::value);
  static_assert(!std::is_pointer<T>::value);
  std::cout<<name<<" is a POD type."<<std::endl;
}
Sedum answered 19/5, 2017 at 14:44 Comment(2)
Thanks. I was just looking into a similar solution. There are so many options, it's hard to pick the right one.Nara
I selected a different answer but yours was a close second. The =delete option seemed a little more obvious to the reader. However, your answer seems more flexible, and I might need this trick in the future. Thank you.Nara
R
0

In fact, I meet the same problem as you.My static_assert(false) perform well in gcc-9. But when I use gcc-7 as my compiler, it will happen. So the best way may be upgrade your gcc or compiler version.

Radiomicrometer answered 17/5, 2023 at 2:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.