简体   繁体   中英

Why the Standard C++ Grammar for Assignment Expression Looks so Weird

From the C++ standard, the grammar for an assignment expression is like this:

assignment-expression:
  conditional-expression
  logical-or-expression assignment-operator assignment-expression
  throw-expression

assignment-operator: one of
   = *= /= %= += -= >>= <<= &= ^= |=

Notice that the left hand side of the "assignment-operator" is "logical-or-expression" ie something like (4 || localvar1) = 5 ; is a valid assignment expression according to the grammar. This doesn't make sense to me. Why they choose a "logical-or-expression" instead of, say, an identifier or id_expression?

Your particular statement, (4 || localvar1) = 5; is invalid (unless operator|| is overloaded), because you can't assign 5 to 4 (4 is an r-value ). You must have an l-value (something that can be assigned to) on the left, such as a reference returned by a function.

For example, say you have some function int& get_my_int() that returns the reference to an integer. Then, you can do this:

`get_my_int() = 5;`

This will set the integer returned by get_my_int() to 5 . Just like in your first post, this MUST be a reference to an integer (and not a value); otherwise, the above statement wouldn't compile.

The grammar is a bit complex, but if you continue unrolling with the previous definitions you will see that assignment expressions are very generic and allow for mostly anything. While the snippet from the standard that you quote focuses on logical-or-expression , if you keep unrolling the definition of that you will find that both the left hand side and right hand side of an assignment can be almost any subexpression (although not literally any ).

The reason as pointed out before is that assignment can be applied to any lvalue expression of enum or fundamental type or rvalue expression of a class type (where operator= is always a member). Many expressions, in a language that allows for operator overloading and that does not define what the type returned from the operator is, can potentially fulfill the needs of assignment, and the grammar must allow all of those uses.

Different rules in the standard will later limit which of the possible expressions that can be generated from the grammar are actually valid or not.

There are actually two interesting things about the C++ grammar for assignment statements, neither of which have to do with the validity of:

 (4 || localvar1) = 5;

That expression is syntactically valid (up to type-checking) because of the parentheses. Any parenthesized expression of reference type is syntactically correct on the left-hand-side of an assignment operator. (And, as has been pointed out, almost any expression which involves a user type or function can be of reference type, as a result of operator overloading.)

What's more interesting about the grammar is that it establishes the left precedence of assignment operators as being lower than almost all other operators, including logical-or, so that the above expression is semantically equivalent to

4 || localvar1 = 5;

even though many readers would interpret the above as 4 || (localvar1 = 5) 4 || (localvar1 = 5) (which would have been totally correct assuming that localvar1 is of a type which can be assigned to by an int , even though said assignment will never happen -- unless, of course, || is overloaded in this context).

So what has a lower precedence on the left hand side of an assignment operator? As I said, very little, but one important exception is ?: :

// Replace the larger of a and b with c
a > b ? a = c : b = c;

is valid and conveniently parenthesis-less. (Many style guides insist on redundant parentheses here, but I personally rather like the unparenthesized version.) This is not the same as right-hand precedence, so that the following also works without parentheses:

// Replace c with the larger of a and b
c = a > b ? a : b;

The only other operators which bind less tightly on the left of an assignment operator than the assignment operator are the , operator and another assignment operator. (In other words, assignment is right-associative unlike almost all other binary operators.) Neither of these are surprising -- in fact, they are so necessary that it's easy to miss how important it is to design a grammar in this way. Consider the following unremarkable for clause:

for (first = p = vec.begin(), last = vec.end(); p < last; ++p)

Here the , is a comma operator, and it clearly needs to bind less tightly than either of the assignments which surround it. (C and C++ are only exceptional in this syntax by having a comma operator; in most languages, , is not considered an operator.) Also, it would obviously be undesirable for the first assignment expression to be parsed as (first = p) = vec.begin() .

The fact that assignment operators associate to the right is unremarkable, but it's worth noting for one historical curiosity. When Bjarne Stroustrup was looking around for operators to overload for I/O streams, he settled on << and >> because, although an assignment operator might have been more natural [1], assignment binds to the right, and a streaming operator must bind to the left ( std::cout << a << b must be (std::cout << a) << b) . However, since << binds much more tightly than assignment, there are a number of gotchas when using the streaming operators. (The one which most recently caught me is that shift binds more tightly than the bitwise operators.)

[Note 1]: I don't have a citation for this, but I remember reading it many years ago in The C++ Programming Language . As I recall, there was not a consensus about assignment operators being natural, but it seems more natural than overloading shift operators to be something completely different from their normal semantics.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM