这段代码中有一些非常不明显的事情:
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 &
是安全的吗?
附注:奖金示例1,额外的复杂性(另请参阅here):
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
发布于 2016-10-21 11:56:31
简而言之:是的,它可以是安全的。但你需要知道会发生什么。
左值常量引用和右值引用可用于延长临时变量的生命周期(减去下面引用的异常)。
顺便说一下,我们已经从您的previous question中了解到,gcc 4.9系列不是此类测试的最佳参考。用gcc 6.1或5.3编译的奖励示例1给出的结果与用clang编译的结果完全相同。这是理所当然的。
引用自N4140 (选定片段):
class.temporary
在两个上下文中,时间语句在与完整表达式结束时不同的点被销毁。..。
第二个上下文是将引用绑定到临时。引用绑定到的临时对象或作为引用绑定到的子对象的完整对象的临时对象在引用的生存期内持续存在,除非:没有与此问题相关的子句
expr.cond
3)否则,如果第二和第三操作数具有不同的类型并且或者具有(可能是cv限定的)类类型,或者如果两者都是除了cv限定之外的相同值类别和相同类型的GL值,则尝试将这些操作数中的每一个转换为另一个的类型。
如果_
E2
可以被隐式地转换(第4条)为类型“_E2
”,则E1
可以被转换为与E2
匹配,受以下约束:在转换中引用必须直接绑定到E1
_T2
_ E2
是PR值的类型,或者如果上面的转换都不能完成并且至少一个操作数具有(可能是cv限定的)类prvalue (即,如果E1
或E2
具有非类类型,或者如果它们都具有类类型,但基础类不同,或者一个不是另一个的基类):如果E1
可以隐式转换为表达式E2
,则可以将E1
转换为表达式E2
在将E2
转换为prvalue时所具有的类型(或者,如果E2
是prvalue,则转换为表达式E2
所具有的类型
..。如果两者都不能转换,则操作数保持不变,并执行进一步的检查,如下所述。如果只有一个转换是可能的,则将该转换应用于所选操作数,并在本节的其余部分中使用转换后的操作数来代替原始操作数。
4)如果第二个和第三个操作数是相同值类别的glvalue,并且具有相同的类型,则结果是该类型和值类别...
5)否则,结果为prvalue。如果第二个和第三个操作数不具有相同的类型,并且其中任何一个操作数具有(可能是cv限定的) class类型...否则,将应用这样确定的转换,并在本节的其余部分中使用转换后的操作数来代替原始操作数。
6)在第二个和第三个操作数上执行左值到右值、数组到指针和函数到指针的标准转换。在这些转换之后,将保持以下其中一项:
因此,第一个示例被很好地定义为执行您所经历过的事情:
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
赋值给x
时将其转换回一个新的、不同的float
。
在你的第二个例子中(奖励1):
float a = 0;
const float b = 0;
const float & x = true ? a : b;
a = 4;
cout << a << ", " << x;
三元运算符不必在a
和b
之间进行任何转换(除了匹配的cv限定符),它会生成一个引用常量浮点数的左值。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
可以隐式转换为类型,则可以将E1
转换为与E2
匹配的类型。如果E2
是prvalue,则具有。现在,这个prvalue与a
具有相同的值,但是是一个不同的对象。x
不是a
的别名。
发布于 2016-11-10 19:56:03
在C++中创建对三元运算符结果的常量引用安全吗?
作为Asker,我会将讨论总结为:对于非模板化的代码,在相当现代的编译器上,这是可以的,并带有警告。对于模板化的代码,作为一个代码审查者,我通常不鼓励这样做。
https://stackoverflow.com/questions/40167231
复制相似问题