在 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 &
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 float
。 float
是 const
的事实无关紧要。
@vsoftco 给定 int a; double b;
,int & x = true ? a : b;
和 double & x = true ? a : b;
都不会编译这是可以/安全的。当您添加const
时,它会编译并创建临时文件。
@Slava 是的,没错,我也是这么想的。但是 OP 提到 在 C++11 中,const'ness“演员”将在没有临时性的情况下发生,因为坦率地说......它可以,因为你正在转向更严格的 const'ness! :) 这让我很困惑。
@neverlastn const int & x = true ? a : b;
编译得很好。所以是的,引用需要const
(否则你试图将非常量引用绑定到临时),但与a
和b
的类型无关,即,你不要他们不需要任何const
。为什么在代码中使用const float b = 0;
而不仅仅是float b = 0;
?
【参考方案1】:
首先,条件运算符的结果要么是指定所选操作数的左值,要么是其值来自所选操作数的纯右值。
T.C. 指出的异常:如果至少一个操作数是类类型并且具有转换为引用的运算符,则结果可能是一个左值,指定由该运算符的返回值指定的对象;如果指定的对象实际上是一个临时对象,则可能会导致悬空引用。这是提供纯右值到左值的隐式转换的此类运算符的问题,而不是条件运算符本身引入的问题。
在这两种情况下,将引用绑定到结果是安全的,将引用绑定到左值或纯右值的通常规则适用。如果引用绑定到纯右值(条件的纯右值结果,或者从条件的左值结果初始化的纯右值),则纯右值的生命周期将延长以匹配引用的生命周期。
在你原来的情况下,条件是:
true ? a : 2.
第二个和第三个操作数是:“float
类型的左值”和“double
类型的纯右值”。这是cppreference summary 中的案例 5,结果是“double
类型的纯右值”。
然后,您的代码使用不同(非引用相关)类型的纯右值初始化 const 引用。这样做的行为是复制初始化与引用相同类型的临时对象。
总而言之,在const float & 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 &
绑定到“const float
类型的左值”,它直接绑定。
所以在const float & x = true ? a : b;
之后,x
直接绑定到a
或b
。
在额外情况 2 中,true ? a_ref : 2.
。第二个和第三个操作数是“const double
类型的左值”和“double
类型的纯右值”,所以结果是“double
类型的纯右值”。
然后你将它绑定到const double & x
,这是一个直接绑定,因为const double
与double
是引用相关的。
所以在const double & x = true ? a_ref : 2.;
之后,x
是一个左值,表示与a_ref
具有相同值的双精度值(但x
未绑定到a
)。
【讨论】:
不完全。struct A int i; operator int&() return i; ; int j; int& p = false ? j : A(); /* oops, dangling */
@T.C.那是不同的 :P 转换到引用运算符有很多这样的问题。 int &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 限定的) 班级类型:E1
或E2
具有非类类型,或者如果它们都具有类类型但基础类不是 相同或一个是另一个的基类):E1
可以转换为匹配E2
ifE1
可以隐式转换为该表达式的类型 如果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;
三元运算符不必在a
和b
之间进行任何转换(匹配的 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[]