43

For a long time I thought that the ternary operator always returns an rvalue. But to my surprise it doesn't. In the following code I don't see the difference between the return value of foo and the return value of the ternary operator.

#include <iostream>
int g = 20 ;

int foo()
{
    return g ;
}

int main()
{
    int i= 2,j =10 ;

    foo()=10 ; // not Ok 
    ((i < 3) ? i : j) = 7; //Ok
    std::cout << i <<","<<j << "," <<g << std::endl ;
}
Soulimane Mammar
  • 1,658
  • 12
  • 20
  • 6
    Ona side note, if you change `int foo()` to `int &foo()`, then the `foo()=10;` also works. – Blaze Feb 14 '19 at 10:42
  • See also https://stackoverflow.com/questions/1082655/conditional-operator-differences-between-c-and-c. As noted there, you could always do `*((i<3) ? &i : &j) = 7;` so the C++ rule could be summarized as "that still works, even without `*&`" – MSalters Feb 14 '19 at 13:31
  • 2
    In C, the conditional operator never yields an lvalue. In C++, it sometimes does. This is one example of why people should refrain from referring to "C/C++". There are all sorts of subtle differences like this that *may* end up being relevant to any particular question. – Brian Bi Feb 14 '19 at 18:11

3 Answers3

40

Both i and j are glvalues (see this value category reference for details).

Then if you read this conditional operator reference we come to this point:

4) If E2 and E3 are glvalues of the same type and the same value category, then the result has the same type and value category

So the result of (i < 3) ? i : j is a glvalue, which can be assigned to.

However doing something like that is really not something I would recommend.

Some programmer dude
  • 400,186
  • 35
  • 402
  • 621
  • I'm wondering about the meaning of lvalue!! can we always put an lvalue at the left hand side of an assignment operator? – Soulimane Mammar Feb 14 '19 at 11:11
  • @SoulimaneMammar Yes, that's where the "l" in "lvalue" comes from, from being on the *left* hand side of an assignment. Another common test is if the address of the value could be taken (using the address-of operator `&`), but that's not always true IIRC (don't remember the details though). – Some programmer dude Feb 14 '19 at 11:14
  • @Siome programmer dude From cppreference a string literal, such as "Hello, world!" is an lvalue!! How can we put a string literal as a left hand side of an assignment – Soulimane Mammar Feb 14 '19 at 11:18
  • 1
    @SoulimaneMammar That's because it's not a *modifiable* lvalue. You can take the address of a literal string (using the address-of operator `&`) though. Reading through the "lvalue properties" list on cppreference, all lvalues can have its address taken, but not all can be assigned to. So I was wrong in my first comment (had it the opposite). – Some programmer dude Feb 14 '19 at 11:26
  • 2
    @SoulimaneMammar The problem there is constness, not value category. You can't put an object with a deleted `operator=` on the left side of an assignment; that doesn't make it not an lvalue. – Sneftel Feb 14 '19 at 11:27
  • If we try to write something stupid like `` "Hello" = "World" `` the compiler will give us a misleading error message like error: lvalue required as left operand of assignment – Soulimane Mammar Feb 14 '19 at 11:35
  • 3
    @Someprogrammerdude - Your first comment represents the origin of the concept of lvalues: Something that can be on the left hand side of an assignment. While the concept has moved beyond that, the name remains. – David Hammen Feb 14 '19 at 11:35
  • 2
    @SoulimaneMammar arrays (such as string literals for example) are tricky lvalues. When their value is used, they implicitly convert to a pointer to first element (this is called decaying) and the decayed pointer is *not* an lvalue. That's the reason for the error diagnostic. More generally though, arrays are simply not assignable in the language even if they aren't const. – eerorika Feb 14 '19 at 11:39
  • 4
    @SoulimaneMammar - Which compiler? gnu c++'s diagnostic for `"Hello" = "World"` is `error: assignment of read-only location '"Hello"'`, while LLVM's is `error: read-only variable is not assignable`. – David Hammen Feb 14 '19 at 11:42
  • 2
    @SoulimaneMammar Literal strings are arrays of `const char`. They decay to *constant* pointers to the first element of the array (constant pointer because the location of a literal string can't be changed either). This pointer is of type `const char * const`. Since the pointer is constant you can't change it, which means it's not possible to put it on the left side of an assignment. – Some programmer dude Feb 14 '19 at 11:47
  • @DavidHammen GCC 4.9.2, – Soulimane Mammar Feb 14 '19 at 16:33
  • 5
    The main use case for this is initialization of references: `int &i = b ? i1 : i2;` – Simon Richter Feb 14 '19 at 20:16
22

The rules for this are detailed in [expr.cond]. There are many branches for several combinations of types and value categories. But ultimately, the expression is a prvalue in the default case. The case in your example is covered by paragraph 5:

If the second and third operands are glvalues of the same value category and have the same type, the result is of that type and value category and it is a bit-field if the second or the third operand is a bit-field, or if both are bit-fields.

Both i and j, being variables names, are lvalue expressions of type int. So the conditional operator produces an int lvalue.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
3

Ternary conditional operator will yield an lvalue, if the type of its second and third operands is an lvalue.

You can use the function template is_lvalue (below) to find out if an operand is an lvalue and use it in the function template isTernaryAssignable to find out if it can be assigned to.

A minimal example:

#include <iostream>
#include <type_traits>

template <typename T>
constexpr bool is_lvalue(T&&) {
  return std::is_lvalue_reference<T>{};
}

template <typename T, typename U>
bool isTernaryAssignable(T&& t, U&& u)
{
    return is_lvalue(std::forward<T>(t)) && is_lvalue(std::forward<U>(u));
}

int main(){
    int i= 2,j =10 ;

    ((i < 3) ? i : j) = 7; //Ok

    std::cout << std::boolalpha << isTernaryAssignable(i, j); std::cout << '\n';
    std::cout << std::boolalpha << isTernaryAssignable(i, 10); std::cout << '\n';
    std::cout << std::boolalpha << isTernaryAssignable(2, j); std::cout << '\n';
    std::cout << std::boolalpha << isTernaryAssignable(2, 10); std::cout << '\n';   
}

Output:

true
false
false
false

LIVE DEMO

Note: The operands you pass to isTernaryAssignable are to be such that they will not undergo decay (For example an array which decays to pointer).

P.W
  • 26,289
  • 6
  • 39
  • 76
  • The live demo may take a while to load, which can be a bit confusing. As can be seen there, in *C++17* the code works as expected. But the same is already the case for **C++11**, see [modified version](https://wandbox.org/permlink/WVYAifjXOkvgkSTu) – Wolf Apr 25 '23 at 13:55