What Is the Value Category of the Operands of C++ Operators When Unspecified

What is the value category of the operands of C++ operators when unspecified?

Yes, it's ill-specified and has been covered before. Basically, every time an lvalue expression is required is enumerated, so we assume that every other operand must be a prvalue expression.

So to answer your questions:

  1. A prvalue.
  2. If it's not specified, it's a prvalue.

The note that is quoted in the linked answer seems to have changed a few times. The quote from §3.10 of the C++11 standard is as follows (and at the current time is identical in the latest draft):

[ Note: The discussion of each built-in operator in Clause 5 indicates the category of the value it yields and the value categories of the operands it expects. For example, the built-in assignment operators expect that the left operand is an lvalue and that the right operand is a prvalue and yield an lvalue as the result. User-defined operators are functions, and the categories of values they expect and yield are determined by their parameter and return types. — end note ]

Here it even says explicitly that the assignment operators expect the right operand to be a prvalue. Of course, this is a note and is therefore non-normative.

Does the operand of unary* operator expect a prvalue

In general, it is CWG1642:

1642. Missing requirements for prvalue operands

Although the note in 6.10 [basic.lval] paragraph 1 states that

The discussion of each built-in operator in Clause 8 [expr] indicates the category of the value it yields and the value categories of the operands it expects

in fact, many of the operators that take prvalue operands do not make that requirement explicit. Possible approaches to address this failure could be a blanket statement that an operand whose value category is not stated is assumed to be a prvalue; adding prvalue requirements to each operand description for which it is missing; or changing the description of the usual arithmetic conversions to state that they imply the lvalue-to-rvalue conversion, which would cover the majority of the omissions.

In particular, [expr.unary.op]/1 wording should be fixed by this PR.

What made i = i++ + 1; legal in C++17?

In C++11 the act of "assignment", i.e. the side-effect of modifying the LHS, is sequenced after the value computation of the right operand. Note that this is a relatively "weak" guarantee: it produces sequencing only with relation to value computation of the RHS. It says nothing about the side-effects that might be present in the RHS, since occurrence of side-effects is not part of value computation. The requirements of C++11 establish no relative sequencing between the act of assignment and any side-effects of the RHS. This is what creates the potential for UB.

The only hope in this case is any additional guarantees made by specific operators used in RHS. If the RHS used a prefix ++, sequencing properties specific to the prefix form of ++ would have saved the day in this example. But postfix ++ is a different story: it does not make such guarantees. In C++11 the side-effects of = and postfix ++ end up unsequenced with relation to each other in this example. And that is UB.

In C++17 an extra sentence is added to the specification of assignment operator:

The right operand is sequenced before the left operand.

In combination with the above it makes for a very strong guarantee. It sequences everything that happens in the RHS (including any side-effects) before everything that happens in the LHS. Since the actual assignment is sequenced after LHS (and RHS), that extra sequencing completely isolates the act of assignment from any side-effects present in RHS. This stronger sequencing is what eliminates the above UB.

(Updated to take into account @John Bollinger's comments.)

How is printf() an expression?

How is printf() an expression?

It's a function call expression.

I always that that function calls aren't really an expression

Function calls are expressions.

They are more like statements

Function calls are not statements. Consider for example ret = printf();. Here, the function call is a subexpression of the assignment expression. Statements cannot be sub expressions - expressions can be sub expressions.

Moreover, the definition they provide mentions operators and operands but none can be seen with printf.

The parentheses are the function call operator. The operands are the function (which is a postfix expression) on the left side of the parentheses and the argument list within the parentheses.

How is the expression characterized by a data type?

Every expression has a type. The type - along with the value category - affects how you can use the expression.

I thought that the data type was related to the return type of the printf function

You thought right. The type of a function call expression is determined by the return type of the function. The type of printf() is int.

The result of an operator that takes an lvalue operand is an lvalue denoting that lvalue operand?

  • x = y returns lvalue referring to left-hand operand.
  • int* j = x = y, this is erroneous because of invalid types of the operands.

But,

  • int* j = &(x = y) :

    works, as x = y returns l-value so this boils down to int *j = &x (value of x is y)

    and similarly, this int& j = x = y also works

Additional links:

For conditional expressions: Return type of '?:' (ternary conditional operator)

What steps should I take to determine the value category of an expression?

If you just want a quick and usually correct answer, consider these rules of thumb:

  • If it's a function or an already existing object, then it's a glvalue.

    • Most glvalues are lvalues.
    • xvalues are the things that can specifically be moved from: casts to an rvalue reference type, or a function call where the function return type is an rvalue reference type (especially std::move and sometimes std::forward).
  • If it's just a value or a way of creating a new object, then it's a prvalue.

But there are some cases where the category could still be unclear. And the above involves some simplifications (in particular, the rules for A.B and A ? B : C are more complicated).

The only really reliable way is to look for your answer in the Standard.

  1. Determine what sort of expression you have in terms of the grammar. A literal? An operator expression? A lambda? Etc.

  2. If the expression is an operator expression, figure out if overload resolution will select some overloaded operator function or a built-in candidate operator, as described in [over.match.oper], [over.oper], and [over.built].

  3. If the expression is actually a call to an overloaded operator function, the value category is determined from the return type of the operator function selected by overload resolution, as described in [expr.call]. In this case, ignore the description of built-in operator behavior for this purpose.

  4. Otherwise, find the section of [expr.prim] or [expr.compound] (see the table of contents) for the grammatical form of the expression. That section will state how the value category of the expression is determined. It will often be necessary to know the types and value categories of any subexpressions, so you may need to follow these rules recursively.

Pre and post increment behaviour in C++

The return type of preincrement is T&, which allows you to modify it (because it's a non-const reference). Postincrement returns T: it's an unnamed value in the t++ context. Therefore, the result is considered const-like so you can't change its state.

If you want to find more information, you can search up lvalues, (p)rvalues etc. but they might be hard to understand.

What type of value do overloaded operators return (for user-defined types): rvalue or lvalue?

[expr.call]/14: A function call is an lvalue if the result type is an lvalue reference type or an rvalue reference to function type, an xvalue if the result type is an rvalue reference to object type, and a prvalue otherwise.

This makes sense, since the result doesn't "have a name". If you returned a reference, the implication would be that it is a reference to some object somewhere that does "have a name" (which is, generally but not always, true).

Then there's this:

[expr.ass]/1: The assignment operator (=) and the compound assignment operators all group right-to-left. All require a modifiable lvalue as their left operand; their result is an lvalue referring to the left operand.

This is saying that an assignment requires an lvalue on the left hand side. So far so good; you've covered this yourself.

How come a non-const function call result works then?

By a special rule!

[over.oper]/8:: [..] Some predefined operators, such as +=, require an operand to be an lvalue when applied to basic types; this is not required by operator functions.

… and = applied to an object of class type invokes an operator function.

I can't readily answer the "why": on the surface of it, it made sense to relax this restriction when dealing with classes, and the original (inherited) restriction on built-ins always seemed a little excessive (in my opinion) but would have had to be kept for compatibility reasons.

But then you have people like Meyers pointing out that it now becomes useful (sort of) to return const values to effectively "undo" this change.

Ultimately I wouldn't try too hard to find a strong rationale either way.



Related Topics



Leave a reply



Submit