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

时间:2016-10-21 02:02:44

标签: c++ c++11 gcc ternary-operator clang++

这段代码中有一些非常明显的事情:

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没有&# 39;超出范围。

我觉得有严重的理由不使用这种形式超出&#34;可维护性和阅读问题&#34;提到here,特别是在编写模板化代码时,您的某些类型及其常规可能无法控制。

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

P.S。奖金示例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

3 个答案:

答案 0 :(得分:9)

首先,条件运算符的结果是指定所选操作数的glvalue,或者是来自所选操作数的值的prvalue。

如T.C所述的异常:如果至少有一个操作数属于类类型并且具有转换为引用运算符,则结果可以是指定由该运算符的返回值指定的对象的左值;如果指定的对象实际上是临时的,则可能会产生悬空参考。对于这样的运算符来说,这是一个问题,它提供了prvalues到lvalues的隐式转换,而不是条件运算符本身引入的问题。

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

在您的原始案例中,条件是:

true ? a : 2.

第二个和第三个操作数是:&#34;类型float的左值&#34;和&#34;类型为double&#34;的prvalue。这是cppreference summary中的案例5,其结果为&#34;类型为double&#34的prvalue。

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

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

在奖励案例1中,条件运算符的第二个和第三个操作数是&#34;类型float的左值&#34;和&#34;左const float&#34;的左值。这是同一个cppreference链接的案例3,

  

两者都是相同值类别的glvalues,并且除了cv-qualification

之外具有相同的类型

行为是第二​​个操作数被转换为&#34;类型为const float&#34的左值; (表示相同的对象),条件的结果是&#34;类型const float的左值&#34;表示所选对象。

然后将const float &绑定到&#34;类型为const float&#34;的左值,它直接绑定。

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

在奖金案例2中,true ? a_ref : 2.。第二个和第三个操作数是const double&#34;类型的左值;和&#34;类型为double&#34;的prvalue,因此结果为&#34;类型为double&#34;的prvalue。

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

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

答案 1 :(得分:3)

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

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

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

来自N4140的报价(选定的片段):

  

[class.temporary]

     

有两种情况下,临时状态被摧毁   不同于完整表达的结束点。 [...]

     

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

  

[expr.cond]

     

3)否则,如果第二个和第三个操作数有不同的类型和   要么具有(可能是cv-qualified)类类型,要么两者都是glvalues   相同值类别和相同类型除外   cv-qualification,尝试转换每个操作数   到另一种的类型。

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

  •   
  • [...]

  •   
  • 如果E2是prvalue,或者上述转换都不能完成且至少有一个操作数具有(可能是cv-qualified)   班级类型:

  •   
    •   
    • 否则(即,如果E1E2具有非类型类型,或者它们都具有类类型但基础类不是   相同或一个是另一个的基类):E1可以转换为匹配   E2如果E1可以隐式转换为表达式的类型   E2如果将E2转换为prvalue(或类型),则会E2   如果float a = 1.; const float & x = true ? a : 2.; // Note: `2.` is a double a = 4.; std::cout << a << ", " << x; 是prvalue,则有,
    •   
  •   
     

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

     

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

     

5)否则,结果是prvalue。如果是第二个和第三个   操作数不具有相同的类型,并且(可能是)   cv-qualified)类类型[...]。否则,转换就这样了   确定应用,并使用转换后的操作数   本节其余部分的原始操作数。

     

6)Lvalue-to-rvalue,array-to-pointer和function-to-pointer   标准转换在第二和第三个操作数上执行。   在这些转换之后,以下之一应该成立:

     
      
  • 第二个和第三个操作数具有算术或枚举类型;执行通常的算术转换以将它们带到a   常见类型,结果属于该类型。
  •   

所以第一个例子很明确地完成了你所经历的事情:

x

float是绑定到a类型的临时对象的引用。它没有引用true ? float : double,因为表达式double被定义为产生double - 然后您才将float转换回新的xfloat a = 0; const float b = 0; const float & x = true ? a : b; a = 4; cout << a << ", " << x; 分配给a时不同b

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

x

三元运算符不必在aa之间进行任何转换(匹配cv限定符除外)并且它产生一个引用const浮点值的左值。 double a = 3; const double & a_ref = a; const double & x = true ? a_ref : 2.; a = 4.; std::cout << a << ", " << x; 别名E1并且必须反映对E2所做的更改。

在第三个例子中(奖金2):

E1

在这种情况下,如果E2可以隐式转换为[...] [E2]的类型,则 a可以转换为匹配x如果a是prvalue ,则有。现在,该prvalue具有与{{1}}相同的值,但是它是一个不同的对象。 {{1}}没有别名{{1}}。

答案 2 :(得分:0)

  

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

作为Asker,我会将讨论总结为;对于非模板化的代码,在非常现代的编译器上,可以使用警告。对于模板化代码,作为代码审阅者,我会一般不鼓励它。