What Are Unevaluated Contexts in C++

What are unevaluated contexts in C++?

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).

C++11 declval: what is unevaluated context?

From the standard [Section 5, #7]

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. [ 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 ]

The contexts where its used are:

  1. Type identification [Section 5.2.8]
  2. Sizeof operator [Section 5.3.3]
  3. noexcept [Section 5.3.7]
  4. Type specifiers For eg decltype(declval<T>()) [Section 7.1.6.2]

So,in layman terms (From what I understand) it cannot be used as a standalone expression like a + b;.

As per comment from Massa below:

In those above mentioned contexts, the expression that denotes the
unevaluated operand is unfolded so that the type of its result (and,
in the case of sizeof, the size of that type) can be determined, but
the evaluation of the expression is never incorporated to the final
program.

Example: typeof(std::cout << "1\n") &x = std::cout; will never
generate the code to print "1\n" to the standard output.

lambdas in unevaluated contexts (Until C++20)

A little background: Linkers don't understand function overloading - they only understand function names like in the C language. That's why C++ compilers mangle your function names. void foo(int) becomes _Z3fooi. The mangled name encodes the types of all the arguments. If your function resulted from a template instantiation, all template arguments get encoded too. If they are themselves template classes, their template arguments get encoded, and so on recursively until the primitive types like ints or function pointers are reached.

Lambdas make this challenging because each lambda is required to have distinct type. This isn't too bad for lambdas defined in functions:

auto foo() { return [](){}; }
auto bar() { return [](){}; }

foo and bar return different types. If you then pass them to another function template, that template's name is mangled as if with foo::__lambda1 or something to that effect.

Letting lambdas appear in decltype would break this mechanism.

void bar(decltype([](){}));
void bar(decltype([](){})) {}

Is this a prototype and a definition? Or are these two different overloads of bar? How to identify them across translation units (how to mangle the names)?

Until now, C++ forbid even asking this question. The paper you linked gives an answer: things like this can't have linkage. Don't even try to mangle them.

Lambdas in unevaluated context (requires expressions)

I'm aware that we can use capture-less lambdas in unevaluated contexts

This is not restricted to stateless lambdas. P0315R4 (Wording for lambdas in unevaluated contexts) removed the restrictions for lambdas (not just stateless ones) to not appear in unevaluated lambdas, whilst modifying the wording for some sections relating to the original intent for the restriction:

The core language changes introduced in this paper are a bit tricky. The reason is that we remove many restrictions on lambda expressions, yet we still want to keep closure types out of the signature of external functions, which would be a nightmare for implementations.

The paper introduces no particular constraints on lambdas w.r.t. concepts, and we may simply turn to [temp.concept]/6 to govern this case:

The constraint-expression of a concept-definition is an unevaluated operand ([expr.context]).

As there is no longer any general restriction on lambas (stateless or not) to not appear as the operand in unevaluated contexts, and there is moreover no explicit restriction in the context of concepts, both concepts C1 and C2 are well-formed.

And indeed, as compared to the restriction on closure types in some declarations, concepts are not instantiated, as per [temp.concept]/5, and does fall under the ODR-considerations that are considered "a bit tricky" in P0315R4.

C++: lambda-expression in unevaluated context

Your code is valid C++20 as written but invalid C++11.

  • Lambda expressions are not allowed in unevaluated contexts (such as decltype) before C++20.
  • Closure types are not default constructible before C++20. In C++20 a closure type that has no capture is default constructible.

Using concepts in an unevaluated context gives inconsistent results

Concept names do not work on the basis of evaluating an expression as we would normally think of it. A concept name resolves to a boolean that tells if the constraint-expression is satisfied or not:

A concept-id is a prvalue of type bool, and does not name a template specialization. A concept-id evaluates to true if the concept's normalized constraint-expression is satisfied ([temp.constr.constr]) by the specified template arguments and false otherwise

Constraint expressions are broken down into atomic pieces. Fortunately, your constraint expression has only one atomic piece: static_cast<T>(true). The way we resolve whether an atomic constraint is satisfied is simple. There are several parts. Part one is:

To determine if an atomic constraint is satisfied, the parameter mapping and template arguments are first substituted into its expression. If substitution results in an invalid type or expression, the constraint is not satisfied.

This is why the compilers allow the first one. static_cast<S>(true) is not a valid expression, as there is no conversion from a bool to an S. Therefore, the atomic constraint is not satisfied, so C<S> is false.

However, static_cast<int>(true) is a valid expression. So we move on to part 2:

Otherwise, the lvalue-to-rvalue conversion is performed if necessary, and E shall be a constant expression of type bool.

And that's where we run into the word "shall". In standard-ese, "shall" means "if the user provides code where this is not the case, there is a compile error". An int is not a "constant expression of type bool". Therefore, the code does not conform to this requirement. And a compile error results.

I imagine that GCC just treats the errors as substitution failures (that or it automatically coerces it into a bool), but the standard requires MSVC/Clang's behavior of erroring out.

this in unevaluated context in static member functions

I don't see how it matters that this appears within an unevaluated context, you've referred to something that doesn't exist in a static member function, so how is the compiler supposed to deduce the type of this within this context?

As a corollary, the type of this in a non-static member function is dependent on the cv-qualifier of said member function, decltype(this) would yield T const* if the member function were const, and T * if it weren't. Thus, the type is dependent on the context of the expression. In your example, the context has no this pointer.

To alleviate the pain of having to name the class you could add an alias for it.

class VeryVeryLongClassName
{
using self = VeryVeryLongClassName;
};

What is an unevaluated operand in the way a 'requires expression' sees it?

An expression still needs to be grammatically correct (as in, in compliance with C++'s grammar as outlined by the standard), evaluated or not. int&&& is not valid per the grammar. (there is no such thing as a ref-qualifier ref-qualifier)

(You can kinda get this by passing an lvalue to a template receiving T&&, but it will become T& via reference collapsing, but then it's clear that it's an rvalue reference to a reference and not the other way around --- int&&& is ambiguous and meaningless)

An unevaluated operand is usually an expression that is used to infer type information, and not for its side effects (whereas a discarded value expression is solely used for its side effects). Other examples of unevaluated operands:

T x;
auto typeInfo = typeid(x); // x is unevaluated
auto x_size = sizeof(x); // x is unevaluated

In the examples above it seems pretty obvious that nothing is really done with x since we're just naming a variable, but consider that the argument to typeid or sizeof can be any expression! The expression will not have any side effects because it is unevaluated; we only care about the resulting expression's type in those cases.

In the case of a requires-expression, the requirement-body (bit between the braces) requires an expression. Since fun(T<int &&&>::x) is grammatically incorrect, it's not an expression and hence a compiler error is emitted. (These expressions can be thought of as an easier & more powerful way to use SFINAE for enabling/disabling templates).

For all the compiler cares, you could have written requires{abra;!?...-cadabra} instead of requires{fun(T<int &&&>::x);} and they're equally invalid.

Why are lambda expressions not allowed in an unevaluated operands but allowed in the unevaluated portions of constant expressions?

The core reason for the unevaluated operands exclusion is covered in C++ Standard Core Language Defect Reports and Accepted Issues #1607. Lambdas in template parameters which seeks to clarify this restriction and states the intention of the restriction in section 5.1.2 was to:

[...] avert the need to deal with them in function template signatures [...]

As the issue documents the current wording actually has a hole since constant expressions allows them in an unevaluated context. But it does not outright state the rationale for this restriction. The desire to avoid name mangling stands out and you can infer that avoiding extending SFINAE was also desired since the proposed resolution seeks to tighten the restriction even though several viable alternatives would have allowed SFINAE. The modified version of 5.1.2 paragraph 2 as follows:

A lambda-expression shall not appear in an unevaluated operand (Clause 5 [expr]), in a template-argument, in an alias-declaration, in a typedef declaration, or in the declaration of a function or function template outside its function body and default arguments [Note: The intention is to prevent lambdas from appearing in a signature —end note]. [Note: A closure object behaves like a function object (20.10 [function.objects]). —end note]

This proposal was accepted and is in N3936(see this answer for a link)

For a more explicit discussion of the rationale to avoid having lambdas as an unevaluated operand. The discussion titled Rationale for lambda-expressions not being allowed in unevaluated contexts on comp.lang.cpp.moderated Daniel Krügler lays out three reasons:

  1. The extreme extension of possible SFINAE cases :

[...]The reason why they became excluded was due to exactly this extreme extension of sfinae cases (you were opening a Pandora box for the compiler)[...]


  1. In many cases it is just useless since each lambda has a unique type, the hypothetical example given:

    template<typename T, typename U>
    void g(T, U, decltype([](T x, T y) { return x + y; }) func);

    g(1, 2, [](int x, int y) { return x + y; });

    The type of the lambda in the declaration and the call are different(by definition) and therefore this can not work.

  2. Name mangling also becomes a problem since once you allow a lambda in a function signature the bodies of the lambda will have to be mangled as well. This means coming up with rules to mangle every possible statement, which would burdensome for at least some implementations.



Related Topics



Leave a reply



Submit