Is it possible to implement always_false in the C++ standard library?
Asked Answered
A

4

23

There are cases where one uses an always_false helper to e.g. cause unconditional static_assert failure if instantiation of some template is attempted:

template <class... T> struct always_false : std::false_type {};

template<class T>
struct UsingThisShouldBeAnError {
  static_assert(always_false<T>::value, "You should not use this!");
};

This helper is necessary because a template definition must (at least theoretically) have at least one set of template parameters for which a valid specialization can be produced in order for the program to be well-formed:

[temp.res]/8: The program is ill-formed, no diagnostic required, if:

  • no valid specialization can be generated for a template [...] and the template is not instantiated, or

[...]

(Writing static_assert(false, "You should not use this!"); above would thus be ill-formed and a compiler could always fire the static assert, even without the template being instantiated, which is not the intention.)

Here is a quick sampling of questions involving this pattern (including further explanation):

It might be useful to have always_false as a tool in the standard library so we don't have to constantly write it again. However, the answer to the following question makes me wonder whether this is even possible:

Dependent non-type parameter packs: what does the standard say?

There the argument is made (also with respect to [temp.res]/8) that std::enable_if_t<T> is always either void or not a type and that it is illegal for anyone to specialize it further. Therefore, a template that relies on the theoretical "specializability" of std::enable_if to avoid the [temp.res]/8 clause actually causes the program to be ill-formed, no diagnostic required.

Coming back to my question: If the standard provided always_false, it would have to forbid library users from specializing it as usual (for obvious reasons). But by the above reasoning, that would defeat the whole point of always_false (namely that it could theoretically be specialized to something other than std::false_type) - with respect to [temp.res]/8 it would be the same as using std::false_type directly.

Am I wrong in this reasoning? Or is it actually impossible for the standard library to provide always_false in a meaningful/useful way (without core language changes)?

Aliped answered 4/9, 2019 at 11:42 Comment(3)
the standard library can do things that are otherwise illegal in user code, so I would say yes, it is possible for the standard library to implement such a construct, even if it were illegal otherwise. Don't quote me though.Endosmosis
@bolov: issue is user's usage. class definition is trivial and doesn't have issue. but once in std, that imposes extra restriction to user (as no specialization unless allowed).Percival
@Percival god, C++ can be have such dark corners...Endosmosis
F
15

To paraphrase Jarod's idea, It could be something like

template <class... T> struct always_false : std::false_type {};

template <> struct always_false</* implementation defined */> : std::true_type{};

Where /* implementation defined */ can be filled by std::_ReservedIdentifer. User code can't access it, since the identifier is reserved to the library, but there exists a specialization that is true. That should avoid questions about the ODR and lambdas in specializations.

Fungoid answered 4/9, 2019 at 12:31 Comment(1)
Accepting this solution because it is more encompassing (and I guess more standard-like).Aliped
P
16

In C++20, with lambda, you might do something like:

template <class... T> struct always_false : std::false_type {};

// To have true, but for a type that user code can't reuse as lambda types are unique.
template <> struct always_false<decltype([](){})> : std::true_type{};

After reflection, I think it is not possible: enclosing template might have other restrictions that the hypothetical type(s) for the specialization should fulfill:

With static_assert(is_enum_v<T> && always_false_v<T>), that type should be an enum.

And even more constrained, with static_assert(is_same_v<T, int> && always_false_v<T>), it is for int.

Edit: C++23 now allows static_assert(false); in non instantiated part :)

Percival answered 4/9, 2019 at 11:58 Comment(5)
Does it really require C++20? Won't something like std::_UglyTypeWeCannotTouch will do?Fungoid
@StoryTeller: Could be, but not sure that a type like that doesn't break requirement "without core language changes".Percival
@Percival how about template <> struct always_false</* implementation defined */> : std::true_type{}; aka unspecified and hidden, not special otherwise.Endosmosis
What bolov said. That's what those _Identifiers are for after all.Fungoid
@StoryTeller it was your idea. If you feel like it do the honors with an answer.Endosmosis
F
15

To paraphrase Jarod's idea, It could be something like

template <class... T> struct always_false : std::false_type {};

template <> struct always_false</* implementation defined */> : std::true_type{};

Where /* implementation defined */ can be filled by std::_ReservedIdentifer. User code can't access it, since the identifier is reserved to the library, but there exists a specialization that is true. That should avoid questions about the ODR and lambdas in specializations.

Fungoid answered 4/9, 2019 at 12:31 Comment(1)
Accepting this solution because it is more encompassing (and I guess more standard-like).Aliped
O
3

All such attempts result in a program that is ill-formed, no diagnostic required.

The clauses that block you from using static_assert(false) make your program ill-formed, no diagnostic required based on the actual possibility of making an actual instantiation, not based on if the compiler can work it out.

These tricks simply make it harder for the compiler to detect the fact your program is ill-formed. The diagnostic they issue is not required, and your ability to bypass the diagnostic being issued just means you made the compiler produce an ill-formed program for which the standard places no restrictions on its behavior.

(Writing static_assert(false, "You should not use this!"); above would thus be ill-formed and a compiler could always fire the static assert, even without the template being instantiated, which is not the intention.)

The exact same conclusion holds for your

template <class... T> struct always_false : std::false_type {};

template<class T>
struct UsingThisShouldBeAnError {
  static_assert(always_false<T>::value, "You should not use this!");
};

I claim there is no valid instantiation of UsingThisShouldBeAnError in the above program.

http://eel.is/c++draft/temp.res#6.1

The program is ill-formed, no diagnostic required, if: (6.1) no valid specialization can be generated for a template [...]"

No valid specialization can be generated for this template.

To avoid this trap, your program must have

template <> struct always_false<SomeListOfTypes> : std::true_type {};

and if an always_false is specified in the standard for which this cannot occur, using always_false from the standard doesn't help you at all. because the standard requires that the specialization "can be generated".

If the only way to instantiate your template is within an ill-formed program, then that is a huge stretch on the word "can". So using reserved or magic types you aren't allowed to use within the true_type specialization is not really plausible.

Stepping back, the purpose of the "ill-formed, ndr" there is because the standard writers want to permit diagnostics of broken code, but don't want to mandate it. Detecting such broken code in general is a halt-hard problem, but simple cases can be detected. And optimizing around the assumption that the code isn't broken can be useful.

All attempts to inject static_assert(false, "message") are working around the intention that C++ code is intended to be valid.

We have a construct for a function for whom successful lookup is an error already in the language.

template<class T>
void should_never_be_found(tag<T>) = delete;

the hole, of course, is the lack of a diagnostic message here.

template<class T>
void should_never_be_found(tag<T>) = delete("Provide an custom ADL overload!");

also, the inability to =delete template specializations.

What you appear to want is:

template<class T>
struct UsingThisShouldBeAnError = delete("You should not use this");

which is direct, intentional, and consistent with how other =delete cases work.

The static_assert bypass requires magic and "tricking" the compiler to bypass parts of the standard explicitly written to prevent you from doing what you want to do.

This =delete("string") and =delete syntax on template classes is missing from the language.

Operant answered 14/10, 2021 at 16:4 Comment(6)
"your program must have". How much code can be added to make a specialization valid? From your answer, it seems none. Do you mean that SFINAE based on a type_traits (such as has_foo) is ill-formed NDR if there doesn't exist a class which fulfills the requirement (template <typename T> void foo(T& t) -> decltype(t.foo()) { return t.foo(); } is potentially ill-formed).Percival
I agree with your overall assessment and your suggestion for the solution (=delete) of the overall problem. However the point of the question is to explore the negation of "if an always_false is specified in the standard for which this cannot occur": How can we specify always_false to allow it to occur, in a useful manner (accepting the contradiction to the name)? I believe that SomeListOfTypes = /*implementation defined*/ hits the mark with "there exists at least one legally usable type that results in true_type, it's just intentionally hard to name it".Aliped
@MaxLanghof We cannot. Either the user is allowed to use a type in SomeListOfTypes in C++ code, in which case it doesn't satisfy your requirement, or the user code cannot, in which case there is no valid specialization for the template. Remember, everything within std header files is literally magic in C++. What code constructs in them are valid is not relevant. I mean, we could go with "the name of the type that works here is implementation defined", but users must be able to name it and using it must be valid.Operant
Any and all hacks around that are either "I am trying to fool the compiler" or "I am trying to confuse or convince the user". Fooling the compiler fundamentally doesn't work, because the standard is being omniscient here; if the only valid type is one that exists if the Collatz conjecture is true, then your program is ill formed depending on the truth value of the Collatz conjecture.Operant
@Yakk-AdamNevraumont The point is that there are options between "using the name is forbidden by punishment of ill-formedness, NDR" and "there's a commonly known way to get std::always_false to become std::true_type". Again, I agree with this being suboptimal (and in the realm of "confuse or convince the user"), but such is life. std::array also must have its actual data member public and user-accessible, and the world doesn't come crashing down.Aliped
"If the only way to instantiate your template is within an ill-formed program,". It would not be ill-formed in compiler/std context though. Might that count?Percival
P
-6

just use option -fdelayed-template-parsing

Palisade answered 4/12, 2020 at 15:6 Comment(1)
-fdelayed-template-parsing is an option of some compilers to enable non-standard behavior. But my question is about a theoretical addition to the standard library while keeping standard-compliant behavior. It's not even related to compilers.Aliped

© 2022 - 2024 — McMap. All rights reserved.