Document number: | P1401r4 | |
---|---|---|
Date: | 2020-10-08 | |
Audience: | EWG | |
Reply-to: | Andrzej Krzemieński <akrzemi1 at gmail dot com> |
bool
This paper proposes to allow conversions from integral types to type bool
in static_assert
s and if constexpr
-statements.
Today | If accepted |
---|---|
if constexpr(bool(flags & Flags::Exec)) | if constexpr(flags & Flags::Exec) |
if constexpr(flags & Flags::Exec != 0) | if constexpr(flags & Flags::Exec) |
static_assert(bool(N)); | static_assert(N); |
static_assert(N % 4 != 0); | static_assert(N % 4); |
Updated wording by adding examples, as instructed by EWG.
Updated references to match the latest Standard Draft.
Updated the table showing compiler implementation to match the most recent compiler versions.
Added more discussion and background information that might help EWG make a more informed decision:
Incorporated feedback from Richard Smith (the submitter of [CWG 2039]). Added recommended resolution and wording.
Extended the discussion and problem analysis. Outlined the reange of possible changes. Not proposing wording anymore: the goal is to obtain the direction from EWG first.
Clang currently fails to compile the following program, and this behavior is Standard-compliant:
enum Flags { Write = 1, Read = 2, Exec = 4 }; template <Flags flags> int f() { if constexpr (flags & Flags::Exec) // fails to compile due to narrowing return 0; else return 1; } int main() { return f<Flags::Exec>(); // when instantiated like this }
This is because, as currently specified, narrowing is not allowed in contextual conversion to bool
in core constant expressions.
If compilers were standard-compliant, even the following code would be ill-formed.
template <std::size_t N> class Array { static_assert(N, "no 0-size Arrays"); // ... }; Array<16> a; // fails to compile in pure C++
All these situations can be fixed by applying a static_cast
to type bool
or comparing the result to 0,
but the fact remains that this behavior is surprising. For instance,
run-time equivalents of the above constructs compile and execute fine:
if (flags & Flags::Exec) // ok {} assert(N); // ok
Note that the proposal only affects the contextual conversions to bool
:
it does not affect implicit conversions to bool
in other contexts.
This paper addresses issue [CWG 2320].
The no-narrowing requirement was added in [CWG 2039], which indicates it was intentional. However, the issue documentation does not state the reason.
The no-narrowing requirement looks justified in the context of noexcept
specifications,
where the "double noexcept
" syntax is so strange that it can be easily misused. For instance,
if I want to say that my function f()
has the same noexcept specification as function g()
,
it doesn't seem impossible that I could mistakenly type this as:
void f() noexcept(g);
To the uninitiated this looks reasonable; and it compiles. Also, if g()
is a constexpr function,
the following works as well:
void f() noexcept(g());
The no-narrowing requirement helps minimize these bugs, so it has merit. But other contexts, like static_assert
,
are only victims thereof.
The definition of contextually converted constant expression of type bool
([expr.const]/10) is used in four places in the standard:
static_assert
if constexpr
explicit(bool)
noexcept(bool)
Note that requires
-clause does not use the definition, as it requires that the expression
"shall be a constant expression of type bool
" ([temp.constr.atomic]/3).
The problems caused by the contextually converted constant expression of type bool
are mostly visible in the first two cases.
In case of explicit(bool)
we expect a type trait to be used as an expression.
Similarly, in the case of noexcept(bool)
we only expect a type trait or a noexcept
-expression.
The following table illustrates where compilers allow a conversion to bool
in a contextually converted constant
expression of type bool against the C++ requirements:
context | gcc 10.1 | clang 10.0.0 | icc 19.0.1 | msvc 19.24 |
---|---|---|---|---|
static_assert | yes | yes | yes | yes |
if constexpr | yes | no | yes | yes |
explicit(bool) | no | no | --* | yes |
noexcept(bool) | no | yes | yes | yes |
* Feature not implemented in this compiler.
Accepting this proposal would be to some extent standardizing the existing practice among compiler vendors.
bool
The following table lists types that are contextually convertible to type bool
:
Type | Allowed in constant expr | true when |
---|---|---|
class with conversion to bool | yes | operator returns true |
class with conversion to a built-in type convertibel to bool | as per rules below | as per below rules |
object/function pointer | no | not null |
function name/reference | no | always |
array name/reference | no | always |
pointer to member | no | not null |
integral type | no, except for 0 and 1 | not zero |
floating-point type | no | not (plus or minus) zero |
nullptr_t | no | never |
unscoped enumeration | no | not zero |
The problem, which this proposal is trying to fix, has only been reported when conversions from integral types or unscoped enumeraiotn types are involved, as for these types such conversion has practical and often used meaning:
We have never encountered a need to check if a floating-point value is exactly +/-0 in this way.
Technically, checking a pointer has a meaning: "is it really pointing to some object/function",
but it is more difficult to imagine a practical use case for it in contextually converted
constant expression of type bool
.
bool
Some have suggested that a conversion to bool
in general should not be considered narrowing,
that bool
should not be treated as a small integral type, and that the conversion to bool
should be treated as a request to classify the states of the object as one of the two categories.
We do not want to go there. Even this seemingly correct reasoning can lead to bugs like this one:
struct Container { explicit Container(bool zeroSize, bool zeroCapacity) {} explicit Container(size_t size, int* begin) {} }; int main() { std::vector<int> v; X x (v.data(), v.size()); // bad order! }
If the feature that prevents narrowing conversions can detect this bug, we would like to use this opportunity.
bool
in constant expressionsAnother situation brought up while discussing this problem was if the following code should be correct:
// a C++03 type trait template <typename T> struct is_small { enum { value = (sizeof(T) == 1) }; }; template <bool small> struct S {}; S<is_small<char>::value> s; // int 1 converted to bool
In constant expressions the situation is different, because whether a conversion is narrowing or not
depends not only on the types but also on the velaues, which are known at compile-time. We think that
after [P0907r4] was adopted,
and bool
has a well defined representation, conversion from 1 to true is now defined as non-narrowing.
assert()
macroAs described in [LWG 3011], currently macro assert()
from the C Standard Library only allows expressions of scalar types, and does not support the concept of expressions
"contextually converted to bool
". We believe that this issue does not interact with our proposal.
For instance, the following example works even under the C definition of macro assert()
:
template <std::size_t N> auto makeArray() { assert(N); // ... }
But it stops working if we change assert(N)
to static_assert(N)
.
Richard Smith — the submitter of
[CWG 2039] —
points out that the original defect report only suggested the modification to noexcept(bool)
context. It suggested wording improvements for static_assert
context,
but they were not supposed to alter the semantics. Thus, the canges to static_assert
semantics got there against the submitters intentions. (The other two contexts —
if constexpr
and explicit(bool)
— were not in the Standard at that time.)
Also, the recommenation from Richard is to apply contextually converted constant expression
of type bool
only to noexcept(bool)
and explicit(bool)
contexts.
There is a two-dimensional space of possible solutions to this problems with two extremal solutions being:
bool
.
(This compromizes the solution in [CWG 2039], whatever its intent was.)The two "degrees of freedom" in the solution space are:
bool
is used; e.g., only in
static_assert
and if constexpr
.bool
only from a subset of types implicitly convertible
to bool
, e.g., only integral and scoped enumeration types.In fact, relaxing only static_assert
and if constexpr
, only for
integral and scoped enumeration types solves all issues that have been reported by users that
we are aware of.
In general, the argument for preventing the narrowing is the to prevent more bugs, whereas the argument for allowing the narrowing is to allow more expressibility and a sense of uniformity between language constructs. In this section we expand on these arguments.
The four contexts that make use of the contextually converted constant expression of type bool
are relatively new, at least compared to constructs inherited from C that use implicit conversions
to bool
. This could be seen as an opportunity to parially fix the past decisions that are
percieved as wrong to allow narrowing conversion in run-time if
-statements and in C
asseret()
s. "We cannot change the past, so at leats let's fix the thing in the new places where breaking
backwards compatibility will be managable."
The type of error that would be prevented is when function invocation is confused with function address. For instance,
I wanted to call if(is_small())
but I omitted parentheses and I got if(is_small)
which still
compiles, but now has unintended semantics.
It is also true that if the programmer is forced to provide an expression that is of type bool
, the code
is easier to understand. Consider:
int test(int x, int y) { if (x + y) // ... if (x == -y) // ... }
The expression in the second if
-statement expresses the intent more clearly.
C++ language has already developed the balance between explicitness and expressiveness. It is mainly inherited from C. The following code is easily understood by C++ programmers.
assert (state & READ_FLAG);
Keeping this work and at the same preventing
static_assert (state & READ_FLAG);
Will make the programmers ask, why is this language so inconsistent? Why am I first told to
learn about the implicit converiosns to bool
only to later find that tey do not
work in similar contexts?
Enforcing a certain explicit, safe programming style is important, especially in bigger
teams. However, this does not have to be settled directly in the language. A common practice
in such cases is to use a separate static analyzer. A characteristic property of such tools
is that any of dozens of unsafe features is defined separately. A user makes the decision
which unsafe features to detect and prevent, and which to allow as harmless. For instance,
Clangs static analyzer clang-tidy
has check
readability-implicit-bool-conversion
.
Using this mechanism, the leader of developement team can decide which checks to enable, and this
way define a save language subset that works for the given project. Not only does it give a better
control of programming constraint trade-offs, but also does not penalize other programmers that favor
expressiveness over explicitness.
For a similar reason, the bug-prone decision in C++ that class constructors by default can be used for implicit conversions is becomming an insignificant issue, because present static analyzers make it easy for programmers to statically detect bugs caused by this behavior. Anyone can enable or define a check like this, and forget the explanations about preserving backwards compatibility.
Similarly, in the case of contextually converted constant expression of type bool
,
rather than introducing an inconsistent constraint against the current implementation practice,
we could rely on static analyzers. We could even think about comming with a list of recommendations
for compiler and tool writers, which usages of the language are considerd "unsafe", in order
to encourage warnings to be reported. We could even go further and introduce a new quality in the
Standard: that implementations should emit a diagnostic message in certain cases for a well defined
program.
A question was asked, how often users complain about this. We believe that this the wrong question.
Given the two contexts where this is really relevant — static_assert
and
if constexpr
— compilers GCC, MSVC and ICC simply do not adhere to the Standard
and allow the narrowing. So there is no reason why users would complain. None of the four compilers
listed earlier even attempts to implement static_assert
in the strictly compliant way.
This is ho vendors avoid disappointing the users.
The only compiler that could
record any complaints is Clang: it allows narrowing in static_assert
but prevents it in
if constexpr
. But even here the motivation for filing a bug is small. People want to get
their job done. If they face the choice between just wrapping their expression in a C-style cast to
bool
and setting up their account in LLVM's bug tracking system and filing all the forms,
they will likely choose the former.
Compilers gcc, icc and clang provide a pedantic mode that eagerly detect any nonconformant code
that would be otherwise allowed by the compiler for programmer's convenience. In gcc and clang the
compiler switch for this mode is -Wpedantic
, in icc it is -pedantic
.
Interestingly, none of the three compilers warns in pedantic mode about this narrowing conversion
to bool
in the contexts that they choose to support in a non-conformant way.
MSVC, which does not appear to have a pedantic mode, does not warn about the narrowing conversion to
bool
in all four contexts even in the higest warning level /WX
.
Programmers often use pedantic mode to learn which constructs they use are Standard-compliant.
In this sense, compilers reaffirm the belief that putting an int
inside
static_assert
is Standard-compliant, and they have been doing this for 9 years now.
If WG21 were to reinforce the decision to prevent narrowing in static_assert
and
if constexpr
it would require of implementers to apply a backwards incompatible
change and potentially break their users' code. The author has asked four vendors — GCC,
Clang, ICC and MSVC — if they would be willing to implement it this is the decision in EWG.
All of them gave the same answer. They would. This would require a two-step process. First, add the constraint in pedantic mode. This way the users can check ahead of time what code will break in the future, and prepare themselves for the impact. Next, introduce the constraint in the default mode, but still allow the conversion in "permissive" mode, so that the users who cannot or will not change their code can still use the newer versions of the compiler.
We propose to apply the contextually converted constant expression of type bool
restriction only in function declarations, that is in noexcept(bool)
and
explicit(bool)
contexts.
In statements (static_assert
and if constexpr
) any conversion
to bool
should be allowed.
The distinction is clear: function declarations, which constitute an interface,
require an additional caution and we require the the expressions to be more restricted.
We also do not expect expressions other than type traits and noexcept()
operator.
In contrast, in statements that are part of function implementation details we allow more liberty
which is compatible with 'runtime equivalents' such as if
-statements and C-asserts.
The propose wording is a delta from N4861.
Insert the following example after [except.spec] para 2.
[Example:
void f() noexcept(sizeof(char[2])); // error: narrowing conversion of value 2 to type bool void g() noexcept(sizeof(char)); // OK, conversion of value 1 to type bool is non-narrowing— end example]
Insert the following example after [dcl.fct.spec] para 4.
[Example:
struct S { explicit(sizeof(char[2])) S(char); // error: narrowing conversion of value 2 to type bool explicit(sizeof(char)) S(bool); // OK, conversion of value 1 to type bool is non-narrowing };— end example]
Change [dcl.dcl] paragraph 6 as follows.
In a static_assert-declaration, the constant-expression shall be
a contextually converted constant expression of typean expression that is a constant expression (7.7) after contextual conversion tobool
(7.7)bool
(7.3). If the value of the expression when so converted istrue
, the declaration has no effect. Otherwise, the program is ill-formed, and the resulting diagnostic message (4.1) shall include the text of the string-literal, if one is supplied, except that characters not in the basic source character set (5.3) are not required to appear in the diagnostic message.[Example:
static_assert(sizeof(int) == sizeof(void*), "wrong pointer size"); static_assert(sizeof(int[2])); // OK, narrowing allowed— end example]
Change the beginning of [stmt.if] paragraph 2 as follows.
If the
if
statement is of the formif constexpr
, the value of the condition shall bea contextually converted constant expression of typean expression that is a constant expression (7.7) after contextual conversion tobool
(7.7)bool
(7.3); this form is called aconstexpr if
statement. If the value of the converted condition is false, the first substatement is a discarded statement, otherwise the second substatement, if present, is a discarded statement. During the instantiation of an enclosing templated entity ([temp.pre]), if the condition is not value-dependent after its instantiation, the discarded substatement (if any) is not instantiated.[Example:
if constexpr (sizeof(int[2])) // OK, narrowing allowed {}— end example]
Jens Maurer reviewed the wording and offered useful suggestions.
Jason Merrill originally reported this issue in CWG reflector. Tomasz Kamiński reviewed the paper and suggested improvements.
Members of EWGI significantly improved the quality of the paper.
bool
" constexpr if
and boolean conversions" assert(E)
inconsistent with C"