为什么 std::ostream 在三元运算符中使用时无法编译?

Why does std::ostream not compile when used in ternary operator?

#include <iostream>
using namespace std;

int main()
{
    std::ostream o(nullptr);
    true ? std::ostream(nullptr) : std::ostream(nullptr); // A
    true ? std::ostream(nullptr) : o; //B
    return 0;
}

我想知道为什么 A 编译正常而 B 失败并出现错误:

prog.cpp: In function ‘int main()’:
prog.cpp:7:33: error: ‘std::basic_ostream<_CharT, _Traits>::basic_ostream(const std::basic_ostream<_CharT, _Traits>&) [with _CharT = char; _Traits = std::char_traits<char>]’ is protected within this context
  true ? o : std::ostream(nullptr);
                                 ^

所以我找到了这个网站:https://docs.microsoft.com/en-us/cpp/cpp/conditional-operator-q?view=vs-2017,它说当三元运算符有参数时("arguments" 我指的是 : 左右两边的参数)然后它可以导致复制参数、强制转换等...这是有道理的,因为 std::ostream 具有定义为 protected 的复制构造函数。所以在 A 中,三元运算符获得相同类型的两个参数,没有进行复制,所以没有问题。而在 B 中,三元运算符获取不同类型的参数,这显然导致需要进行复制,而这在 std::ostream 中是不允许的。目前看来一切正常。

但后来我尝试了这个:

#include <iostream>
using namespace std;

int main()
{
    std::ostream o(nullptr);
    std::ostream & oRef = o;
    std::ostream && oRRef = std::ostream(nullptr);
    true ? std::ostream(nullptr) : std::ostream(nullptr); // A
    true ? std::ostream(nullptr) : o; //B
    true ? std::ostream(nullptr) : oRef; // C
    true ? std::ostream(nullptr) : oRRef; // D
    true ? std::ostream(nullptr) : std::move(oRRef); // E
    return 0;
}

CDE 也因类似错误而失败。

所以我有几个问题。表达式的类型是什么:std::ostream(nullptr)?当三元运算符具有不同类型时,三元运算符会复制其参数,但当它们属于相同类型时永远不会复制,这是真的吗?还有什么我错过或需要知道的吗?

该标准包含一些关于如何计算条件表达式的复杂规则 ([expr.cond])。但我不会在这里引用这些规则,而是要解释你应该如何看待它们。

条件表达式的结果可以是左值、xvalue 或 prvalue。但是必须在编译时知道它是其中的哪一个。 (表达式的值类别永远不会取决于运行时发生的情况)。很容易看出,如果第二个和第三个表达式都是同一类型的左值,那么结果也可以成为左值,并且不必进行复制。如果第二个和第三个表达式都是同一类型的纯右值,那么从 C++17 开始,也不必进行复制——T 类型的纯右值表示类型对象的延迟初始化T,编译器根据条件简单地选择传递这两个纯右值中的哪一个最终用于初始化对象。

但是当一个表达式是左值而另一个是相同类型的纯右值时,则结果必须是纯右值。如果标准说结果是左值,那将是不合逻辑的,因为条件可能会导致选择纯右值操作数,而您不能将纯右值转换为左值。但是你可以反过来做。所以标准说,当一个操作数是左值而另一个是相同类型的纯右值时,左值必须进行左值到右值的转换。并且如果您尝试对 std::ostream 对象进行左值到右值的转换,由于复制构造函数被删除,程序将格式错误。

因此:

  • 在A中,两个操作数都是纯右值,所以没有左值到右值的转换;这在 C++17 中是可以的(但在 C++14 中不是)。
  • 在 B 中,o 需要左值到右值的转换,因此无法编译。
  • 在 C 中,oRef 是一个左值,所以仍然需要左值到右值的转换,所以这也不会编译。
  • 在 D 中,oRRef 仍然是左值(因为右值引用的名称是左值)。
  • 在 E 中,一个参数是 prvalue,一个是 xvalue。 xvalue 仍然需要转换为 prvalue 以使结果成为 prvalue。

E 的情况值得进一步说明。在 C++11 中很明显,如果一个参数是一个 xvalue 而另一个是相同类型的纯右值,则 xvalue 必须经过(误导性命名)左值到右值的转换以产生纯右值。在 std::ostream 的情况下,这使用了受保护的移动构造函数(因此程序违反了成员访问控制)。在 C++17 中,人们可以考虑更改规则,而不是将 xvalue 转换为纯右值,而是将纯右值具体化以产生一个 xvalue,从而避免移动的需要。但是这个改变没有明显的好处,它是否是最合理的行为值得怀疑,所以这可能就是它没有被做出的原因(如果它被考虑的话)。