为什么和何时三元运算符会返回一个左值?

43

很长一段时间以来,我一直认为三目运算符始终会返回一个rvalue。但令我惊讶的是它并不是这样。在下面的代码中,我看不出 foo 的返回值和三目运算符的返回值之间有什么区别。

#include <iostream>
int g = 20 ;

int foo()
{
    return g ;
}

int main()
{
    int i= 2,j =10 ;

    foo()=10 ; // not Ok 
    ((i < 3) ? i : j) = 7; //Ok
    std::cout << i <<","<<j << "," <<g << std::endl ;
}

7
顺带一提,如果将int foo() 改为 int &foo(),那么 foo()=10; 也能够起作用。 - Blaze
三元运算符始终返回一个右值,令人惊讶的是它确实如此。您是否意味着“不会”,因为当两侧都是左值时,它可以返回左值。 - Jarod42
请参见 https://dev59.com/eXNA5IYBdhLWcg3wH6EW。正如在那里所指出的,您始终可以执行 *((i<3) ? &i : &j) = 7;,因此C++规则可以概括为“即使没有 *& 也仍然有效”。 - MSalters
2
在C语言中,条件运算符永远不会产生lvalue。但在C++中,有时会产生。这就是为什么人们应该避免使用“C/C++”这种说法的一个例子。像这样的微妙差别可能对任何特定问题都有影响。 - Brian Bi
3个回答

40

ij均为glvalue(详情请参见这个值类别的参考文献)。

如果您阅读此条件运算符参考文献,我们会得出以下结论:

4) 如果E2和E3是相同类型且相同值类别的glvalue,则结果具有相同的类型和值类别

因此(i < 3) ? i : j的结果是一个glvalue,可以进行赋值操作。

然而,我真的不建议像这样做任何事情。


3
@Someprogrammerdude - 你的第一条评论代表了lvalue概念的起源:可以出现在赋值符号左边的东西。虽然这个概念已经超越了那个范围,但名称仍然保持不变。 - David Hammen
2
@SoulimaneMammar 数组(例如字符串字面量)是棘手的lvalue。当它们的值被使用时,它们会隐式转换为第一个元素的指针(这称为衰减),而衰减的指针不是lvalue。这就是错误诊断的原因。然而,更一般地说,即使它们不是const,数组在语言中也不能被赋值。 - eerorika
4
使用哪个编译器?GNU C++对于"Hello"="World"的诊断是error: assignment of read-only location '"Hello"',而LLVM的是error: read-only variable is not assignable - David Hammen
2
@SoulimaneMammar 字符串字面量是 const char 数组。它们会衰减为指向数组第一个元素的常量指针(常量指针,因为字符串字面量的位置也无法更改)。该指针的类型为 const char * const。由于指针是常量,您无法更改它,这意味着无法将其放在赋值语句的左侧。 - Some programmer dude
5
这的主要用途是初始化引用:int &i = b ? i1 : i2; - Simon Richter
显示剩余7条评论

22
这个规则在[expr.cond]详细说明。对于几种类型和值类别的组合,有许多分支。但最终,在默认情况下该表达式是一个prvalue。你的示例中情况由第5段覆盖:

如果第二个和第三个操作数是相同值类别的glvalues,并且具有相同的类型,则结果是该类型和值类别,并且如果第二个或第三个操作数是位字段或者两个都是位字段,则它是一个位字段。

由于“i”和“j”都是变量名,因此它们是类型为“int”的lvalue表达式。所以条件运算符产生了一个“int” lvalue。

3
三元条件运算符将生成一个左值,如果它的第二个和第三个操作数的类型是左值。您可以使用函数模板is_lvalue(以下)来查找操作数是否为左值,并在函数模板isTernaryAssignable中使用它来找出是否可以对其进行赋值。
一个简单的示例:
```c++ template constexpr bool is_lvalue(T&&) { return std::is_lvalue_reference::value; }
template constexpr bool isTernaryAssignable() { return is_lvalue(std::declval()) && is_lvalue(std::declval()); } ```
#include <iostream>
#include <type_traits>

template <typename T>
constexpr bool is_lvalue(T&&) {
  return std::is_lvalue_reference<T>{};
}

template <typename T, typename U>
bool isTernaryAssignable(T&& t, U&& u)
{
    return is_lvalue(std::forward<T>(t)) && is_lvalue(std::forward<U>(u));
}

int main(){
    int i= 2,j =10 ;

    ((i < 3) ? i : j) = 7; //Ok

    std::cout << std::boolalpha << isTernaryAssignable(i, j); std::cout << '\n';
    std::cout << std::boolalpha << isTernaryAssignable(i, 10); std::cout << '\n';
    std::cout << std::boolalpha << isTernaryAssignable(2, j); std::cout << '\n';
    std::cout << std::boolalpha << isTernaryAssignable(2, 10); std::cout << '\n';   
}

输出:

true
false
false
false

实时演示

注意: 传递给isTernaryAssignable的操作数应该不会经历衰减(例如,一个会衰减为指针的数组)。


现场演示可能需要一些时间才能加载,这可能有点令人困惑。如图所示,在C++17中,代码按预期工作。但是C++11的情况也是如此,可以查看修改后的版本 - Wolf

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接