One example that frequently comes to mind is :
sizeof expression, where it doesn't evaluates the expression but determines the size by static type. For example :
int func();
sizeof(func());
This is my limit of thinking, so if there are other unevaluated contexts, then what are they?
Fortunately, the standard has a handy list of those (§ 5 [expr] ¶ 8):
In some contexts, unevaluated operands appear (5.2.8, 5.3.3, 5.3.7, 7.1.6.2). An unevaluated operand is not evaluated. An unevaluated operand is considered a full-expression.
Let's look at these in detail.
I will use the following declarations in my examples. The declared functions are never defined anywhere so if a call to them appears in an evaluated context, the program is ill-formed and we will get a link-time error. Calling them in an unevaluated context is fine, however.
int foo(); // never defined anywhere
struct widget
{
virtual ~widget();
static widget& get_instance(); // never defined anywhere
};
typeid
§ 5.2.8 [expr.typeid] ¶ 3:
When typeid is applied to an expression other than a glvalue of a polymorphic class type, the result refers to a std::type_info object representing the static type of the expression. Lvalue-to-rvalue (4.1), array-to-pointer (4.2), and function-to-pointer (4.3) conversions are not applied to the expression. If the type of the expression is a class type, the class shall be completely-defined. The expression is an unevaluated operand
(Clause 5).
Note the emphasized exception for polymorphic classes (a class with at least one virtual member).
Therefore, this is okay
typeid( foo() )
and yields a std::type_info object for int while this
typeid( widget::get_instance() )
is not and will probably produce a link-time error. It has to evaluate the operand because the dynamic type is determined by looking up the vptr at run-time.
<rant>I find it quite confusing that the fact whether or not the static type of the operand is polymorphic changes the semantics of the operator in such dramatic, yet subtle, ways.</rant>
sizeof
§ 5.3.3 [expr.sizeof] ¶ 1:
The sizeof operator yields the number of bytes in the object representation of its operand. The operand is either an expression, which is an unevaluated operand (Clause 5), or a parenthesized type-id. The sizeof operator shall not be applied to an expression that has function or incomplete type, to an enumeration type whose underlying type is not fixed before all its enumerators have been declared, to the parenthesized name of such types, or to a glvalue that designates a bit-field.
The following
sizeof( foo() )
is perfectly fine and equivalent to sizeof(int).
sizeof( widget::get_instance() )
is allowed too. Note, however, that it is equivalent to sizeof(widget) and therefore probably not very useful on a polymorphic return type.
noexcept
§ 5.3.7 [expr.unary.noexcept] ¶ 1:
The noexcept operator determines whether the evaluation of its operand, which is an unevaluated operand (Clause 5), can throw an exception (15.1).
The expression
noexcept( foo() )
is valid and evaluates to false.
Here is a more realistic example that is also valid.
void bar() noexcept(noexcept( widget::get_instance() ));
Note that only the inner noexcept is the operator while the outer is the specifier.
decltype
§ 7.1.6.2 [dcl.type.simple] ¶ 4.4:
The operand of the decltype specifier is an unevaluated operand (Clause 5).
The statement
decltype( foo() ) n = 42;
declares a variable n of type int and initializes it with the value 42.
auto baz() -> decltype( widget::get_instance() );
declares a function baz that takes no arguments and returns a widget&.
And that's all there are (as of C++14).
The standard term is an unevaluated operand and you can find it in [expr]
In some contexts, unevaluated operands appear (5.2.8, 5.3.3, 5.3.7, 7.1.6.2). An unevaluated operand is not evaluated. An unevaluated operand is considered a full-expression. [ Note: In an unevaluated operand, a non-static class member may be named (5.1) and naming of objects or functions does not, by itself, require that a definition be provided (3.2). —end note ]
5.2.8 covers typeid
5.3.3 covers sizeof
5.3.7 covers noexcept
7.1.6.2 covers simple type specifiers such as auto and decltype and POD types like int, char, double etc.
Related
One example that frequently comes to mind is :
sizeof expression, where it doesn't evaluates the expression but determines the size by static type. For example :
int func();
sizeof(func());
This is my limit of thinking, so if there are other unevaluated contexts, then what are they?
Fortunately, the standard has a handy list of those (§ 5 [expr] ¶ 8):
In some contexts, unevaluated operands appear (5.2.8, 5.3.3, 5.3.7, 7.1.6.2). An unevaluated operand is not evaluated. An unevaluated operand is considered a full-expression.
Let's look at these in detail.
I will use the following declarations in my examples. The declared functions are never defined anywhere so if a call to them appears in an evaluated context, the program is ill-formed and we will get a link-time error. Calling them in an unevaluated context is fine, however.
int foo(); // never defined anywhere
struct widget
{
virtual ~widget();
static widget& get_instance(); // never defined anywhere
};
typeid
§ 5.2.8 [expr.typeid] ¶ 3:
When typeid is applied to an expression other than a glvalue of a polymorphic class type, the result refers to a std::type_info object representing the static type of the expression. Lvalue-to-rvalue (4.1), array-to-pointer (4.2), and function-to-pointer (4.3) conversions are not applied to the expression. If the type of the expression is a class type, the class shall be completely-defined. The expression is an unevaluated operand
(Clause 5).
Note the emphasized exception for polymorphic classes (a class with at least one virtual member).
Therefore, this is okay
typeid( foo() )
and yields a std::type_info object for int while this
typeid( widget::get_instance() )
is not and will probably produce a link-time error. It has to evaluate the operand because the dynamic type is determined by looking up the vptr at run-time.
<rant>I find it quite confusing that the fact whether or not the static type of the operand is polymorphic changes the semantics of the operator in such dramatic, yet subtle, ways.</rant>
sizeof
§ 5.3.3 [expr.sizeof] ¶ 1:
The sizeof operator yields the number of bytes in the object representation of its operand. The operand is either an expression, which is an unevaluated operand (Clause 5), or a parenthesized type-id. The sizeof operator shall not be applied to an expression that has function or incomplete type, to an enumeration type whose underlying type is not fixed before all its enumerators have been declared, to the parenthesized name of such types, or to a glvalue that designates a bit-field.
The following
sizeof( foo() )
is perfectly fine and equivalent to sizeof(int).
sizeof( widget::get_instance() )
is allowed too. Note, however, that it is equivalent to sizeof(widget) and therefore probably not very useful on a polymorphic return type.
noexcept
§ 5.3.7 [expr.unary.noexcept] ¶ 1:
The noexcept operator determines whether the evaluation of its operand, which is an unevaluated operand (Clause 5), can throw an exception (15.1).
The expression
noexcept( foo() )
is valid and evaluates to false.
Here is a more realistic example that is also valid.
void bar() noexcept(noexcept( widget::get_instance() ));
Note that only the inner noexcept is the operator while the outer is the specifier.
decltype
§ 7.1.6.2 [dcl.type.simple] ¶ 4.4:
The operand of the decltype specifier is an unevaluated operand (Clause 5).
The statement
decltype( foo() ) n = 42;
declares a variable n of type int and initializes it with the value 42.
auto baz() -> decltype( widget::get_instance() );
declares a function baz that takes no arguments and returns a widget&.
And that's all there are (as of C++14).
The standard term is an unevaluated operand and you can find it in [expr]
In some contexts, unevaluated operands appear (5.2.8, 5.3.3, 5.3.7, 7.1.6.2). An unevaluated operand is not evaluated. An unevaluated operand is considered a full-expression. [ Note: In an unevaluated operand, a non-static class member may be named (5.1) and naming of objects or functions does not, by itself, require that a definition be provided (3.2). —end note ]
5.2.8 covers typeid
5.3.3 covers sizeof
5.3.7 covers noexcept
7.1.6.2 covers simple type specifiers such as auto and decltype and POD types like int, char, double etc.
Consider:
#include <compare>
template<class=void>
constexpr int f() { return 1; }
unsigned int x;
using T = decltype(x <=> f());
GCC and MSVC accept the declaration of T. Clang rejects it, with the following error message:
<source>:7:26: error: argument to 'operator<=>' cannot be narrowed from type 'int' to 'unsigned int'
using T = decltype(x <=> f());
^
1 error generated.
(live demo)
If the template-head (template<class=void>) is removed, or if f is explicitly or implicitly instantiated before the declaration of T, then Clang accepts it. For example, Clang accepts:
#include <compare>
template<class=void>
constexpr int f() { return 1; }
unsigned x;
auto _ = x <=> f();
using T = decltype(x <=> f());
(live demo)
Which compiler is correct, and why?
Clang is correct per N4861.
[temp.inst]/5:
Unless a function template specialization is a declared specialization, the function template specialization is implicitly instantiated when the specialization is referenced in a context that requires a function definition to exist or if the existence of the definition affects the semantics of the program.
[temp.inst]/8:
The existence of a definition of a variable or function is considered to affect the semantics of the program if the variable or function is needed for constant evaluation by an expression ([expr.const])
[expr.const]/15:
A function or variable is needed for constant evaluation if it is:
a constexpr function that is named by an expression ([basic.def.odr]) that is potentially constant evaluated, or
a variable [...].
[expr.const]/15:
An expression or conversion is potentially constant evaluated if it is:
a manifestly constant-evaluated expression,
a potentially-evaluated expression ([basic.def.odr]),
an immediate subexpression of a braced-init-list,
an expression of the form & cast-expression that occurs within a templated entity, or
a subexpression of one of the above that is not a subexpression of a nested unevaluated operand.
[expr.const]/5:
An expression E is a core constant expression unless the evaluation of E, following the rules of the abstract machine ([intro.execution]), would evaluate one of the following:
[...]
an invocation of an undefined constexpr function;
[dcl.init.list]/7:
A narrowing conversion is an implicit conversion
[...]
from an integer type or unscoped enumeration type to an integer type that cannot represent all the values of the original type, except where the source is a constant expression whose value after integral promotions will fit into the target type
[expr.spaceship]/4:
If both operands have arithmetic types, or one operand has integral type and the other operand has unscoped enumeration type, the usual arithmetic conversions are applied to the operands.
Then:
If a narrowing conversion is required, other than from an integral type to a floating-point type, the program is ill-formed.
[expr.arith.conv]:
[T]he usual arithmetic conversions [...] are defined as follows:
[...]
Otherwise, the integral promotions ([conv.prom]) shall be performed on both operands.
Then the following rules shall be applied to the promoted operands:
[...]
Otherwise, if the operand that has unsigned integer type has rank greater than or equal to the rank of the type of the other operand, the operand with signed integer type shall be converted to the type of the operand with unsigned integer type.
Since x <=> f() in decltype(x <=> f()) does not satisfy the criteria of being "potentially constant evaluated", f is not "needed for constant evaluation". Therefore, the existence of the definition of f<> is not considered to affect the semantics of the program. Therefore, this expression does not instantiate the definition of f<>.
Therefore, in the original example, f() is a call to undefined constexpr function, which is not a constant expression.
Per the usual arithmetic conversions, in x <=> f(), f() (of type int) is converted to unsigned int. When f() is not a constant expression, this conversion is a narrowing conversion, which renders the program ill-formed.
If f is not a function template, or if its definition has been instantiated, then f() is a constant expression, and because the result of f() fits into unsigned int, the conversion from f() to unsigned int is not a narrowing conversion, and thus the program is well-formed.
One example that frequently comes to mind is :
sizeof expression, where it doesn't evaluates the expression but determines the size by static type. For example :
int func();
sizeof(func());
This is my limit of thinking, so if there are other unevaluated contexts, then what are they?
Fortunately, the standard has a handy list of those (§ 5 [expr] ¶ 8):
In some contexts, unevaluated operands appear (5.2.8, 5.3.3, 5.3.7, 7.1.6.2). An unevaluated operand is not evaluated. An unevaluated operand is considered a full-expression.
Let's look at these in detail.
I will use the following declarations in my examples. The declared functions are never defined anywhere so if a call to them appears in an evaluated context, the program is ill-formed and we will get a link-time error. Calling them in an unevaluated context is fine, however.
int foo(); // never defined anywhere
struct widget
{
virtual ~widget();
static widget& get_instance(); // never defined anywhere
};
typeid
§ 5.2.8 [expr.typeid] ¶ 3:
When typeid is applied to an expression other than a glvalue of a polymorphic class type, the result refers to a std::type_info object representing the static type of the expression. Lvalue-to-rvalue (4.1), array-to-pointer (4.2), and function-to-pointer (4.3) conversions are not applied to the expression. If the type of the expression is a class type, the class shall be completely-defined. The expression is an unevaluated operand
(Clause 5).
Note the emphasized exception for polymorphic classes (a class with at least one virtual member).
Therefore, this is okay
typeid( foo() )
and yields a std::type_info object for int while this
typeid( widget::get_instance() )
is not and will probably produce a link-time error. It has to evaluate the operand because the dynamic type is determined by looking up the vptr at run-time.
<rant>I find it quite confusing that the fact whether or not the static type of the operand is polymorphic changes the semantics of the operator in such dramatic, yet subtle, ways.</rant>
sizeof
§ 5.3.3 [expr.sizeof] ¶ 1:
The sizeof operator yields the number of bytes in the object representation of its operand. The operand is either an expression, which is an unevaluated operand (Clause 5), or a parenthesized type-id. The sizeof operator shall not be applied to an expression that has function or incomplete type, to an enumeration type whose underlying type is not fixed before all its enumerators have been declared, to the parenthesized name of such types, or to a glvalue that designates a bit-field.
The following
sizeof( foo() )
is perfectly fine and equivalent to sizeof(int).
sizeof( widget::get_instance() )
is allowed too. Note, however, that it is equivalent to sizeof(widget) and therefore probably not very useful on a polymorphic return type.
noexcept
§ 5.3.7 [expr.unary.noexcept] ¶ 1:
The noexcept operator determines whether the evaluation of its operand, which is an unevaluated operand (Clause 5), can throw an exception (15.1).
The expression
noexcept( foo() )
is valid and evaluates to false.
Here is a more realistic example that is also valid.
void bar() noexcept(noexcept( widget::get_instance() ));
Note that only the inner noexcept is the operator while the outer is the specifier.
decltype
§ 7.1.6.2 [dcl.type.simple] ¶ 4.4:
The operand of the decltype specifier is an unevaluated operand (Clause 5).
The statement
decltype( foo() ) n = 42;
declares a variable n of type int and initializes it with the value 42.
auto baz() -> decltype( widget::get_instance() );
declares a function baz that takes no arguments and returns a widget&.
And that's all there are (as of C++14).
The standard term is an unevaluated operand and you can find it in [expr]
In some contexts, unevaluated operands appear (5.2.8, 5.3.3, 5.3.7, 7.1.6.2). An unevaluated operand is not evaluated. An unevaluated operand is considered a full-expression. [ Note: In an unevaluated operand, a non-static class member may be named (5.1) and naming of objects or functions does not, by itself, require that a definition be provided (3.2). —end note ]
5.2.8 covers typeid
5.3.3 covers sizeof
5.3.7 covers noexcept
7.1.6.2 covers simple type specifiers such as auto and decltype and POD types like int, char, double etc.
This is the example in [expr.prim.id]/2 (2.3):
struct S {
int m;
};
int i = sizeof(S::m); // OK
int j = sizeof(S::m + 42); // OK
I'd like to know which paragraph in the Standard (N4713) validates the expression sizeof(S::m + 42) used above.
As #M.M points out, the operand of sizeof can be any expression (8.5.2.3.1):
[...] The operand is either an expression, which is an unevaluated operand, or a parenthesized type-id.
There are a few other restrictions noted in 8.5.2.3, but none apply here.
Note that it mentions that it can be an unevaluated operand -- this makes possible to use the non-static class member S::m here, see (8.2.3.1):
An unevaluated operand is not evaluated. [ Note: In an unevaluated operand, a non-static class member may be named ([expr.prim]) and naming of objects or functions does not, by itself, require that a definition be provided ([basic.def.odr]). An unevaluated operand is considered a full-expression. — end note ]
Which mentions the paragraph that you were referring to in the question (8.4.4.2.3):
An id-expression that denotes a non-static data member or non-static member function of a class can only be used: [...] if that id-expression denotes a non-static data member and it appears in an unevaluated operand.
This is a follow-up of the question: What does the void() in decltype(void()) mean exactly?.
decltype(void()) compiles fine and what the void() means in this case is explained in the above mentioned question (actually in the answer).
On the other side, I noticed that decltype(void{}) doesn't compile.
What's the difference between them (in the context of a decltype at least)?
Why doesn't the second expression compile?
For completeness, it follows a minimal (not-)working example:
int main() {
// this doesn't compile
//decltype(void{}) *ptr = nullptr;
// this compiles fine
decltype(void()) *ptr = nullptr;
(void)ptr;
}
void() is interpreted as type-id when used with sizeof.
void() is interpreted as an expression when used with decltype.
I don't think void{} is valid in any context. It is neither a valid type-id nor a valid expression.
(Building on discussion in the question comments)
Note: I was referencing C++17 or close-to for the answer. C++14 works the same way, the text difference is noted near the end of the answer.
void() is a special exception. See N4618 5.2.3 [expr.type.conv], emphasis mine:
1 A simple-type-specifier (7.1.7.2) or typename-specifier (14.6) followed by a parenthesized optional expression-list or by a braced-init-list (the initializer) constructs a value of the specified type given the initializer. If the type is a placeholder for a deduced class type, it is replaced by the return type of the function selected by overload resolution for class template deduction (13.3.1.8) for the remainder of this section.
2 If the initializer is a parenthesized single expression, the type conversion expression is equivalent (in definedness, and if defined in meaning) to the corresponding cast expression (5.4). If the type is (possibly cv-qualified) void and the initializer is (), the expression is a prvalue of the specified type that performs no initialization. Otherwise, the expression is a prvalue of the specified type whose result object is direct-initialized (8.6) with the initializer. For an expression of the form T(), T shall not be an array type.
So void() is only valid because it's explicitly identified in [expr.type.conv]/2 as no initialization. void{} doesn't meet that exception, so it attempts to be a direct-initialized object.
tl;dr here through C++14 notes: You cannot direct-initialize a void object.
Rabbit-holing through N4618 8.6 [dcl.init] to see what's actually going on with void{}
8.6/16 => direct-initialization
8.6/17.1 => list-initialized, jump to 8.6.4
8.6.4/3.10 => value-initialized
void() would have shortcut the above three with 8.6/11 => value-initialized, and then rejoined the trail, which is why the special exception in [expr.type.conv] is needed.
8.6/8.4 => zero-initialized
Now, 8.6/6 defines zero-initialize for:
scalar
non-union class type
union type
array type
reference type
N4618 3.9 [basic.types]/9 defines scalar:
Arithmetic types (3.9.1), enumeration types, pointer types, pointer to member types(3.9.2), std::nullptr_t, and cv-qualified versions of these types (3.9.3) are collectively called scalar types.
N4618 3.9.1 [basic.fundamental]/8 defines arithmetic types:
Integral and floating types are collectively called arithmetic types.
So void is not an arithmetic type, so it's not a scalar, so it cannot be zero-initialized, so it cannot be value-initialized, so it cannot be direct-initialized, so the expression is not valid.
Apart from the initialisation, void() and void{} would work the same way, producing a prvalue expression of type void. Even though you cannot have a result object for an incomplete type, and void is always incomplete:
N4618 3.9.1 [basic.fundamental]/9 (bold mine):
A type cv void is an incomplete type that cannot be completed; such a type has an empty set of values.
decltype specifically allows incomplete types:
N4618 7.1.7.2 [decl.type.simple]/5 (bold mine):
If the operand of a decltype-specifier is a prvalue, the temporary materialization conversion is not applied (4.4) and no result object is provided for the prvalue. The type of the prvalue may be incomplete.
In C++14, N4296 5.2.3 [expr.type.conv] is differently worded. The braced form was almost an afterthought to the parenthesised version:
A simple-type-specifier (7.1.6.2) or typename-specifier (14.6) followed by a parenthesized expression-list constructs a value of the specified type given the expression list. If the expression list is a single expression, the type conversion expression is equivalent (in definedness, and if defined in meaning) to the corresponding cast expression (5.4). If the type specified is a class type, the class type shall be complete. If the expression list specifies more than a single value,the type shall be a class with a suitably declared constructor(8.5,12.1), and the expression T(x1, x2, ...) is equivalent in effect to the declaration T t(x1, x2, ...); for some invented temporary variable t, with the result being the value of t as a prvalue.
The expression T(), where T is a simple-type-specifier or typename-specifier for a non-array complete object type or the (possibly cv-qualified) void type, creates a prvalue of the specified type, whose value is that produced by value-initializing (8.5) an object of type T; no initialization is done for the void() case. [Note: if T is a non-class type that is cv-qualified, the cv-qualifiers are discarded when determining the type of the resulting prvalue (Clause 5). —end note]
Similarly, a simple-type-specifier or typename-specifier followed by a braced-init-list creates a temporary object of the specified type direct-list-initialized (8.5.4) with the specified braced-init-list, and its value is that temporary object as a prvalue.
The effect is the same for our purposes, the change relates to P0135R1 Wording for guaranteed copy elision through simplified value categories, which removed temporary object creation from expressions. Instead, the context of the expression provides a result object to be initialised by the expression, if the context needs it.
As noted above, decltype (unlike sizeof or typeid) doesn't provide a result-object for the expression, which is why void() works even though it cannot initialise a result object.
I feel that the exception in N4618 5.2.3 [expr.type.conv] ought to be applied to void{} too. It means that guidelines around {} get more complex. See for example ES.23: Prefer the {} initializer syntax in the C++ Core Guidelines, which would currently recommend decltype(void{}) over decltype(void()). decltype(T{}) is more likely to be where this one bites you...