24

There's something quite non-obvious going on in this code:

float a = 1.;

const float & x = true ? a : 2.; // Note: `2.` is a double

a = 4.;

std::cout << a << ", " << x;

both clang and gcc output:

4, 1

One would naively expect the same value printed twice but this isn't the case. The issue here has nothing to do with the reference. There are some interesting rules dictating the type of ? :. If the two arguments are of different type and can be casted, they will by using a temporary. The reference will point to the temporary of ? :.

The example above compiles fine and it might or might not issue a warning while compiling with -Wall depending on the version of your compiler.

Here's an example on how easy it's to get this wrong in legitimate-looking code:

template<class Iterator, class T>
const T & min(const Iterator & iter, const T & b)
{
    return *iter < b ? *iter : b;
}

int main()
{
    // Try to remove the const or convert to vector of floats
    const std::vector<double> a(1, 3.0);

    const double & result = min(a.begin(), 4.);

    cout << &a[0] << ", " << &result;
}

If your logic after this code assumes that any changes on a[0] will be reflected to result, it will be wrong in cases where ?: creates a temporary. Also, if at some point you make a pointer to result and you use it after result goes out of scope, there will be a segmentation fault despite the fact that your original a hasn't gone out of scope.

I feel there're serious reasons NOT to use this form beyond "maintainability and reading issues" mentioned here especially while writing templated code where some of your types and their const'ness might be out of your control.

So my question is, is it safe to use const &s on ternary operators?

P.S. Bonus example 1, extra complications (see also here):

float a = 0;
const float b = 0;
const float & x = true ? a : b;

a = 4;
cout << a << ", " << x;

clang output:

4, 4

gcc 4.9.3 output:

4, 0

With clang this example compiles and runs as expected but with up to recent versions of gcc (

P.S.2 Bonus example 2, great for interviews ;) :

double a = 3;

const double & a_ref = a;

const double & x = true ? a_ref : 2.;

a = 4.;

std::cout << a << ", " << x;

output:

4, 3
Community
  • 1
  • 1
neverlastn
  • 2,164
  • 16
  • 23
  • What if one type is `int` and one `double`, without any `const`? In that case a temp will be created and you'll end up pointing to the temporary. Is there anything changed in C++11 wrt this? – vsoftco Oct 21 '16 at 02:17
  • @vsoftco there is already suche example here I think - `double` vs `float`. The fact that `float` is `const` is irrelevant. – Slava Oct 21 '16 at 02:20
  • @vsoftco Given `int a; double b;` , both `int & x = true ? a : b;` and `double & x = true ? a : b;` don't compile which is ok/safe. When you add `const`, it compiles and creates temporaries. – neverlastn Oct 21 '16 at 02:21
  • @Slava Yes, exactly, that's what I thought too. But OP mentions *In C++11 the const'ness "cast" will happen without a temporary because frankly... it can since you're moving to more strict const'ness! :)* which confused me. – vsoftco Oct 21 '16 at 02:21
  • @neverlastn `const int & x = true ? a : b;` compiles fine. So yes, `const` is needed for the reference (otherwise you're trying to bind a non-const ref to a temporary), but has nothing to do with the type of `a` and `b`, i.e., you don't need any `const` on them. Why do you use `const float b = 0;` in your code and not simply `float b = 0;`? – vsoftco Oct 21 '16 at 02:23
  • @vsoftco - yes, you're right. That mention remained out of some thoughts I had around [this](http://stackoverflow.com/questions/40160904/difference-on-address-of-const-reference-to-ternary-operator-between-clang-and-g). There is a const-related problem in some cases. – neverlastn Oct 21 '16 at 02:24
  • @vsoftco about const vs non const OP is refering to gcc bug from http://stackoverflow.com/questions/40160904/address-of-conditional-ternary-operator-in-c-clang-gcc?noredirect=1#comment67602888_40160904 – Slava Oct 21 '16 at 02:24
  • @neverlastn I think you should remove `const` from `float b = 0;` to avoid confusion – Slava Oct 21 '16 at 02:25
  • Info: with `gcc version 5.4.1 20160904 (Ubuntu 5.4.1-2ubuntu1~14.04)` the result of the upper-most code is `4, 4`. May have been a bug in `gcc 4.9.3`? – Adrian Colomitchi Oct 21 '16 at 02:25
  • @AdrianColomitchi - Indeed I think that people reproduced this up to version < v5.3 – neverlastn Oct 21 '16 at 02:26
  • A reference is not the referent, it has its own lifetime entirely distinct from the referent, may occupy storage, etc. But that's completely irrelevant to the discussion here. May I suggest removing the irrelevant quote of the horribly wrong C++ FAQ? – Ben Voigt Oct 21 '16 at 02:32
  • @BenVoigt Done. – neverlastn Oct 21 '16 at 02:38
  • Interesting variation on the mixed-type case: http://rextester.com/INBP8067 – Ben Voigt Oct 21 '16 at 02:44
  • @BenVoigt Yes - your interesting variation gives different values on older gcc and clang/newer gcc's. – neverlastn Oct 21 '16 at 02:49

3 Answers3

10

First of all, the result of the conditional operator is either a glvalue designating the selected operand, or a prvalue whose value comes from the selected operand.

Exception as noted by T.C.: if at least one operand is of class type and has a conversion-to-reference operator, the result may be an lvalue designating the object designated by the return value of that operator; and if the designated object is actually a temporary, a dangling reference may result. This is a problem with such operators that offer implicit conversion of prvalues to lvalues, not a problem introduced by the conditional operator per se.

In both cases it is safe to bind a reference to the result, the usual rules for binding a reference to an lvalue or a prvalue apply. If the reference binds to a prvalue (either the prvalue result of the conditional, or a prvalue initialized from the lvalue result of the conditional), the lifetime of the prvalue is extended to match the lifetime of the reference.


In your original case, the conditional is:

true ? a : 2.

The second and third operand are: "lvalue of type float" and "prvalue of type double". This is case 5 in the cppreference summary, with the result being "prvalue of type double".

Then, your code initializes a const reference with a prvalue of a different (non-reference-related) type. The behaviour of this is to copy-initialize a temporary of the same type as the reference.

In summary, after const float & x = true ? a : 2.;, x is an lvalue denoting a float whose value is the result of converting a to double and back. (Not sure off the top of my head whether that is guaranteed to compare equal to a). x is not bound to a.


In bonus case 1, the second and third operand of the conditional operator are "lvalue of type float" and "lvalue of type const float". This is case 3 of the same cppreference link,

both are glvalues of the same value category and have the same type except for cv-qualification

The behavour is that the second operand is converted to "lvalue of type const float" (denoting the same object), and the result of the conditional is "lvalue of type const float" denoting the selected object.

Then you bind const float & to "lvalue of type const float", which binds directly.

So after const float & x = true ? a : b;, x is directly bound to either a or b.


In bonus case 2, true ? a_ref : 2. . The second and third operands are "lvalue of type const double" and "prvalue of type double", so the result is "prvalue of type double".

Then you bind this to const double & x, which is a direct binding since const double is reference-related to double.

So after const double & x = true ? a_ref : 2.; , then x is an lvalue denoting a double with the same value as a_ref (but x is not bound to a).

M.M
  • 138,810
  • 21
  • 208
  • 365
  • Not quite. `struct A { int i; operator int&() { return i; } }; int j; int& p = false ? j : A(); /* oops, dangling */` – T.C. Oct 21 '16 at 03:11
  • @T.C. That's different :P conversion-to-reference operator have a lot of such problems. `int &p = A();` has the same problem, the conditional operator does not introduce a problem. I interpreted that as the gist of OP's question. But it is good to note all the same. – M.M Oct 21 '16 at 03:13
3

In short: yes, it can be safe. But you need to know what to expect.

Lvalue const references and rvalue references can be used to prolong the lifetime of temporary variables (minus exceptions referenced below).

By the way, we have already learned from your previous question that gcc 4.9 series is not the best reference for this kind of test. Bonus example 1 compiled with gcc 6.1 or 5.3 gives exactly the same result as compiled with clang. As it's supposed to.

Quotes from N4140 (selected fragments):

[class.temporary]

There are two contexts in which temporaries are destroyed at a different point than the end of the full-expression. [...]

The second context is when a reference is bound to a temporary. The temporary to which the reference is bound or the temporary that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference except: [no relevant clauses to this question]

[expr.cond]

3) Otherwise, if the second and third operand have different types and either has (possibly cv-qualified) class type, or if both are glvalues of the same value category and the same type except for cv-qualification, an attempt is made to convert each of those operands to the type of the other.

  • If E2 is an lvalue: E1 can be converted to match E2 if E1 can be implicitly converted (Clause 4) to the type “lvalue reference to T2”, subject to the constraint that in the conversion the reference must bind directly to an lvalue

  • [...]

  • If E2 is a prvalue or if neither of the conversions above can be done and at least one of the operands has (possibly cv-qualified) class type:

    • Otherwise (i.e., if E1 or E2 has a nonclass type, or if they both have class types but the underlying classes are not either the same or one a base class of the other): E1 can be converted to match E2 if E1 can be implicitly converted to the type that expression E2 would have if E2 were converted to a prvalue (or the type it has, if E2 is a prvalue)

[...] If neither can be converted, the operands are left unchanged and further checking is performed as described below. If exactly one conversion is possible, that conversion is applied to the chosen operand and the converted operand is used in place of the original operand for the remainder of this section.

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

5) Otherwise, the result is a prvalue. If the second and third operands do not have the same type, and either has (possibly cv-qualified) class type [...]. Otherwise, the conversions thus determined are applied, and the converted operands are used in place of the original operands for the remainder of this section.

6) Lvalue-to-rvalue, array-to-pointer, and function-to-pointer standard conversions are performed on the second and third operands. After those conversions, one of the following shall hold:

  • The second and third operands have arithmetic or enumeration type; the usual arithmetic conversions are performed to bring them to a common type, and the result is of that type.

So the first example is well defined to do exactly what you experienced:

float a = 1.;
const float & x = true ? a : 2.; // Note: `2.` is a double
a = 4.;
std::cout << a << ", " << x;

x is a reference bound to a temporary object of type float. It does not refer to a, because the expression true ? float : double is defined to yield a double - and only then you're converting that double back to a new and different float when assigning it to x.


In your second example (bonus 1):

float a = 0;
const float b = 0;
const float & x = true ? a : b;

a = 4;
cout << a << ", " << x;

the ternary operator doesn't have to do any conversions between a and b (except for matching cv-qualifiers) and it yields an lvalue referring to a const float. x aliases a and must reflect the changes made to a.


In the third example (bonus 2):

double a = 3;
const double & a_ref = a;
const double & x = true ? a_ref : 2.;

a = 4.;
std::cout << a << ", " << x;

In this case E1 can be converted to match E2 if E1 can be implicitly converted to the type that [...] [E2] has, if E2 is a prvalue. Now, that prvalue has the same value as a, but is a different object. x does not alias a.

Community
  • 1
  • 1
krzaq
  • 16,240
  • 4
  • 46
  • 61
1

Is it safe to create a const reference to result of ternary operator in C++?

As the Asker, I would summarize the discussion to; It's ok for non-templated code, on quite modern compilers, with Warnings on. For templated code, as a code reviewer, I would, in general discourage it.

neverlastn
  • 2,164
  • 16
  • 23