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

Posted

技术标签:

【中文标题】在 C++ 中创建对三元运算符结果的 const 引用是不是安全?【英文标题】:Is it safe to create a const reference to result of ternary operator in C++?在 C++ 中创建对三元运算符结果的 const 引用是否安全? 【发布时间】:2017-03-03 05:01:26 【问题描述】:

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

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 并未超出范围,但仍会出现分段错误。

我认为除了here 提到的“可维护性和阅读问题”之外,我认为有很多理由不使用此表单,尤其是在编写模板代码时,您的某些类型及其常量可能超出您的控制范围。

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

附:奖励示例 1,额外的复杂性(另见 here):

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

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

叮当输出:

4, 4

gcc 4.9.3 输出:

4, 0

使用 clang,这个例子可以按预期编译和运行,但使用最新版本的 gcc (

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

double a = 3;

const double & a_ref = a;

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

a = 4.;

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

输出:

4, 3

【问题讨论】:

如果一种类型是int 和一种double,而没有任何const 怎么办?在这种情况下,将创建一个临时文件,您最终将指向临时文件。 C++11 有什么变化吗? @vsoftco 我认为这里已经有这样的例子 - double vs floatfloatconst 的事实无关紧要。 @vsoftco 给定 int a; double b;int &amp; x = true ? a : b;double &amp; x = true ? a : b; 都不会编译这是可以/安全的。当您添加const 时,它会编译并创建临时文件。 @Slava 是的,没错,我也是这么想的。但是 OP 提到 在 C++11 中,const'ness“演员”将在没有临时性的情况下发生,因为坦率地说......它可以,因为你正在转向更严格的 const'ness! :) 这让我很困惑。 @neverlastn const int &amp; x = true ? a : b; 编译得很好。所以是的,引用需要const(否则你试图将非常量引用绑定到临时),但与ab 的类型无关,即,你不要他们不需要任何const。为什么在代码中使用const float b = 0; 而不仅仅是float b = 0; 【参考方案1】:

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

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

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


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

true ? a : 2.

第二个和第三个操作数是:“float 类型的左值”和“double 类型的纯右值”。这是cppreference summary 中的案例 5,结果是“double 类型的纯右值”。

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

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


在额外情况 1 中,条件运算符的第二个和第三个操作数是“float 类型的左值”和“const float 类型的左值”。这是同一个 cppreference 链接的案例 3,

两者都是相同值类别的glvalues并且除了cv-qualification之外具有相同的类型

行为是将第二个操作数转换为“const float 类型的左值”(表示相同的对象),条件的结果是“const float 类型的左值”表示选择的对象。

然后你将const float &amp;绑定到“const float类型的左值”,它直接绑定。

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


在额外情况 2 中,true ? a_ref : 2. 。第二个和第三个操作数是“const double 类型的左值”和“double 类型的纯右值”,所以结果是“double 类型的纯右值”。

然后你将它绑定到const double &amp; x,这是一个直接绑定,因为const doubledouble 是引用相关的。

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

【讨论】:

不完全。 struct A int i; operator int&amp;() return i; ; int j; int&amp; p = false ? j : A(); /* oops, dangling */ @T.C.那是不同的 :P 转换到引用运算符有很多这样的问题。 int &amp;p = A();也有同样的问题,条件运算符不引入问题。我将其解释为 OP 问题的要点。但最好注意一下。【参考方案2】:

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

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

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

引自 N4140(部分片段):

[class.temporary]

临时对象在两种情况下被销毁 与完整表达式的结尾不同的点。 [...]

第二个上下文是引用绑定到临时的。这 引用绑定到的临时对象或临时对象 引用绑定到的子对象的完整对象 在引用的生命周期内持续存在,除了:[无相关 这个问题的子句]

[expr.cond]

3) 否则,如果第二个和第三个操作数的类型不同,并且 要么具有(可能是 cv 限定的)类类型,要么两者都是 glvalues 具有相同的价值类别和相同的类型,除了 cv-qualification,尝试转换每个操作数 到对方的类型。

如果E2 是左值:E1 可以转换为匹配E2 如果E1 可以隐式转换(第 4 条)类型为“左值引用” 到T2”,受制于转换中的约束 引用必须直接绑定到左值

[...]

如果E2 是纯右值,或者以上两种转换都无法完成且至少有一个操作数具有(可能是 cv 限定的) 班级类型:

否则(即,如果 E1E2 具有非类类型,或者如果它们都具有类类型但基础类不是 相同或一个是另一个的基类):E1 可以转换为匹配 E2 if E1 可以隐式转换为该表达式的类型 如果 E2 被转换为纯右值(或它的类型),E2 将会有 有,如果E2 是prvalue)

[...] 如果两者都不能转换,则操作数保持不变并且 如下所述执行进一步检查。如果恰好一个 转换是可能的,该转换应用于所选 操作数和转换后的操作数用于代替原始操作数 本节其余部分的操作数。

4) 如果第二个和第三个操作数是相同值的glvalues 类别并具有相同的类型,结果是该类型和值 类别 [...]

5) 否则,结果为纯右值。如果第二个和第三个 操作数没有相同的类型,并且有(可能 cv 限定)类类型 [...]。否则,转换因此 确定被应用,并且转换的操作数被使用到位 本节其余部分的原始操作数。

6) 左值到右值、数组到指针和函数到指针 对第二个和第三个操作数执行标准转换。 在这些转换之后,应满足以下条件之一:

第二个和第三个操作数具有算术或枚举类型;执行通常的算术转换以将它们带到 common 类型,结果就是那个类型。

所以第一个例子的定义很好,完全符合你的经验:

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 分配给@987654344 @。


在您的第二个示例中(奖励 1):

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

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

三元运算符不必在ab 之间进行任何转换(匹配的 cv 限定符除外),它会产生一个引用 const 浮点数的左值。 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 是prvalue。现在,该纯右值与a 具有相同的值,但是是不同的对象。 x 没有别名 a

【讨论】:

【参考方案3】:

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

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

【讨论】:

以上是关于在 C++ 中创建对三元运算符结果的 const 引用是不是安全?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用三元运算符有条件地初始化 const char* arr[]

如何在 Laravel 中创建对 db 的查询?

在领域中创建对对象的引用的正确方法

在Firestore中创建对Cloud Storage文档的引用

在 C# 中创建元组时编译错误,没有括号包围三元运算符

如何在 ActiveRecords 中创建对 Ruby 中对象的引用?