Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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
like image 782
neverlastn Avatar asked Oct 21 '16 02:10

neverlastn


People also ask

Why we should not use ternary operator?

They simply are. They very easily allow for very sloppy and difficult to maintain code. Very sloppy and difficult to maintain code is bad. Therefore a lot of people improperly assume (since it's all they've ever seen come from them) that ternary operators are bad.

Is using ternary operator good practice?

The ternary operator is a common tool that you'll see a lot in JavaScript code. It can make your code concise but it can also make your code unreadable if you don't use it properly. Try to keep the ternaries simple and readable.

How do you handle 3 conditions in a ternary operator?

The conditional (ternary) operator is the only JavaScript operator that takes three operands: a condition followed by a question mark ( ? ), then an expression to execute if the condition is truthy followed by a colon ( : ), and finally the expression to execute if the condition is falsy.

Which is more preferable if or ternary operator?

Moreover, as has been pointed out, at the byte code level there's really no difference between the ternary operator and if-then-else. As in the above example, the decision on which to choose is based wholly on readability.


3 Answers

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

like image 70
M.M Avatar answered Oct 17 '22 17:10

M.M


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.

like image 42
krzaq Avatar answered Oct 17 '22 15:10

krzaq


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.

like image 1
neverlastn Avatar answered Oct 17 '22 17:10

neverlastn