在 C++ 中创建对三元运算符结果的 const 引用是否安全?

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

这段代码中发生了一些不太明显的事情:

float a = 1.;

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

a = 4.;

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

clang 和 gcc 输出:

4, 1

人们会天真地期望相同的值被打印两次,但事实并非如此。这里的问题与引用无关。有一些有趣的规则规定了 ? : 的类型。如果两个参数的类型不同并且可以转换,则它们将使用临时对象。引用将指向 ? :.

的临时值

上面的示例可以正常编译,并且在使用 -Wall 编译时可能会发出警告,也可能不会发出警告,具体取决于您的编译器版本。

这里有一个例子,说明在看起来合法的代码中出错是多么容易:

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;
}

如果您在此代码之后的逻辑假设 a[0] 上的任何更改都将反映到 result,那么在 ?: 创建临时文件的情况下将是错误的。此外,如果在某个时候你指向 result 并在 result 超出范围后使用它,那么尽管你的原始 a 没有'没有超出范围。

我觉得除了 "maintainability and reading issues" 提到的 here 之外,还有其他重要的理由不使用这种形式,尤其是在编写模板代码时,您的某些类型及其常量可能不受您的控制.

所以我的问题是,在三元运算符上使用 const & 安全吗?

P.S。奖励示例 1,额外的并发症(另见 ):

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

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

clang 输出:

4, 4

gcc 4.9.3 输出:

4, 0

使用 clang 此示例可以按预期编译和运行,但使用的是最新版本的 gcc (

P.S.2 额外示例 2,非常适合面试 ;) :

double a = 3;

const double & a_ref = a;

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

a = 4.;

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

输出:

4, 3

首先,条件运算符的结果要么是指定所选操作数的左值,要么是其值来自所选操作数的纯右值。

T.C 指出的异常:如果至少一个操作数是 class 类型并且具有转换为引用的运算符,则结果可能是左值指定由该运算符的 return 值指定的对象;如果指定的对象实际上是一个临时对象,则可能会导致悬空引用。这是此类提供纯右值到左值隐式转换的运算符的问题,而不是条件运算符本身引入的问题。

在这两种情况下,将引用绑定到结果是安全的,将引用绑定到左值或纯右值的通常规则适用。如果引用绑定到纯右值(条件的纯右值结果,或从条件的左值结果初始化的纯右值),纯右值的生命周期将延长以匹配引用的生命周期。


在你原来的情况下,条件是:

true ? a : 2.

第二个和第三个操作数是:"lvalue of type float"和"prvalue of type double"。这是 cppreference summary 中的情况 5,结果为 "prvalue of type double".

然后,您的代码使用不同(非引用相关)类型的纯右值初始化 const 引用。其行为是复制初始化与引用相同类型的临时对象。

总之,在const float & x = true ? a : 2.;之后,x是一个表示float的左值,其值是将a转换为double并返回的结果. (不确定是否保证比较等于 a)。 x 未绑定到 a


在bonus case 1中,条件运算符的第二个和第三个操作数是"lvalue of type float"和"lvalue of type const float"。这是相同 cppreference link,

的情况 3

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

行为是将第二个操作数转换为"lvalue of type const float"(表示相同的对象),条件的结果是"lvalue of type const float"表示选择的对象。

然后你绑定const float &到"lvalue of type const float",直接绑定。

所以在const float & x = true ? a : b;之后,x直接绑定到ab


在奖励案例 2 中,true ? a_ref : 2.。第二个和第三个操作数是"lvalue of type const double"和"prvalue of type double",所以结果是"prvalue of type double"。

然后将其绑定到 const double & x,这是直接绑定,因为 const doubledouble 相关。

所以在 const double & x = true ? a_ref : 2.; 之后, x 是一个左值,表示与 a_ref 具有相同值的双精度值(但 x 未绑定到 a).

简而言之:是的,它可以是安全的。但是你需要知道会发生什么。

左值常量引用和右值引用可用于延长临时变量的生命周期(减去下面引用的例外)。

顺便说一下,我们已经从您的 那里了解到,gcc 4.9 系列并不是此类测试的最佳参考。使用 gcc 6.1 或 5.3 编译的额外示例 1 给出的结果与使用 clang 编译的结果完全相同。正如它应该的那样。

引自N4140(节选):

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

所以第一个示例定义明确,完全符合您的经验:

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

x 是绑定到类型 float 的临时对象的引用。它不引用 a,因为表达式 true ? float : double 被定义为产生 double - 只有这样你才能将 double 转换回新的和不同的 float 分配给 x.


在你的第二个例子中(奖励 1):

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

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

三元运算符不必在 ab 之间进行任何转换(匹配 cv 限定符除外),它会产生一个引用 const float 的左值。 x 别名 a 并且必须反映对 a.

所做的更改

第三个例子(奖励2):

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

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

在这种情况下 E1 可以转换为匹配 E2 如果 E1 可以隐式转换为 [...] [E2] 有,如果 E2 是纯右值 。现在,纯右值与 a 具有相同的值,但是是不同的对象。 x 不使用别名 a

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

作为提问者,我会将讨论总结为;对于非模板代码,在相当现代的编译器上,带有警告,这是可以的。对于模板化代码,作为代码审查者,我通常不鼓励它。