c++ - 为什么 std::istringstream 在三元 (?:) 运算符中的解析结果与 std::ifstream 不同?

标签 c++ c++11 conditional-operator

我习惯于编写使用文件名或从 std::cin 读取的小型命令行工具,因此我已经使用这种模式很长一段时间了:

int main(int argc, char* argv[])
{
    std::string filename;

    // args processing ...

    std::ifstream ifs;

    if(!filename.empty())
        ifs.open(filename);

    std::istream& is = ifs.is_open() ? ifs : std::cin;

    std::string line;
    while(std::getline(is, line))
    {
        // process line...
    }

    return 0;
}

在阅读了 Stack Overflow 上的问题后,我尝试修改我常用的模式,以满足从文件或 std::istringstream 读取的需要。令我惊讶的是,它无法编译并给出以下错误:

temp.cpp:164:47: error: invalid initialization of non-const reference of type ‘std::istream& {aka std::basic_istream<char>&}’ from an rvalue of type ‘void*’
      std::istream& is = ifs.is_open() ? ifs : iss; // won't compile

在我看来,它正在尝试将 std::istringstream 对象 (iss) 转换为 bool 值并获取其运算符 void*().

int main(int argc, char* argv[])
{
    std::string filename;
    std::string data;

    // args processing ...

    std::ifstream ifs;
    std::istringstream iss;

    if(!filename.empty())
        ifs.open(filename);

    std::istream& is = ifs.is_open() ? ifs : iss; // won't compile

    std::string line;
    while(std::getline(is, line))
    {
        // process line...
    }

    return 0;
}
  1. 为什么它对待 std::istringstream 的方式与 std::cinstd::ifstream 不同?它们都源自 std::istream

    然后我记得已经转换了我的模式以适应三种可能性,从文件、字符串或 std::cin 读取。我记得这很有效(尽管非常笨拙)。因此,将三重解决方案应用于这个问题,我想出了一个完全有效的软糖:

    int main(int argc, char* argv[])
    {
        std::string filename;
        std::string data;
    
        // args processing ...
    
        std::ifstream ifs;
        std::istringstream iss;
    
        if(!filename.empty())
            ifs.open(filename);
    
        std::istream& is = ifs.is_open() ? ifs : true ? iss : std::cin; // fudge works
    
        std::string line;
        while(std::getline(is, line))
        {
            // process line...
        }
    
        return 0;
    }
    
  2. 为什么这种软糖会起作用? GCC 是否违反了三元运算符 (?:) 解析其类型的任何规则?或者我错过了什么?

最佳答案

最小化示例:

class A { };
class B : public A { };
class C : public A { };

int main() {
    B b;
    C c;
    A& refA = true? b : c;
}

Clang 报告:

main.cpp:13:19: error: incompatible operand types ('B' and 'C')
    A& refA = true? b : c;

相关规则见标准§5.16 [expr.cond]/p3-6:

3 Otherwise, if the second and third operand have different types and either has (possibly cv-qualified) class type, or if both are glvalues of the same value category and the same type except for cv-qualification, an attempt is made to convert each of those operands to the type of the other. The process for determining whether an operand expression E1 of type T1 can be converted to match an operand expression E2 of type T2 is defined as follows:

  • If E2 is an lvalue: E1 can be converted to match E2 if E1 can be implicitly converted (Clause 4) to the type “lvalue reference to T2”, subject to the constraint that in the conversion the reference must bind directly (8.5.3) to an lvalue.
  • If E2 is an xvalue: E1 can be converted to match E2 if E1 can be implicitly converted to the type “rvalue reference to T2”, subject to the constraint that the reference must bind directly.
  • If E2 is a prvalue or if neither of the conversions above can be done and at least one of the operands has (possibly cv-qualified) class type:
    • if E1 and E2 have class type, and the underlying class types are the same or one is a base class of the other: E1 can be converted to match E2 if the class of T2 is the same type as, or a base class of, the class of T1, and the cv-qualification of T2 is the same cv-qualification as, or a greater cv-qualification than, the cv-qualification of T1. If the conversion is applied, E1 is changed to a prvalue of type T2 by copy-initializing a temporary of type T2 from E1 and using that temporary as the converted operand.
    • Otherwise (i.e., if E1 or E2 has a nonclass type, or if they both have class types but the underlying classes are not either the same or one a base class of the other): E1 can be converted to match E2 if E1 can be implicitly converted to the type that expression E2 would have if E2 were converted to a prvalue (or the type it has, if E2 is a prvalue).

Using this process, it is determined whether the second operand can be converted to match the third operand, and whether the third operand can be converted to match the second operand. If both can be converted, or one can be converted but the conversion is ambiguous, the program is ill-formed. If neither can be converted, the operands are left unchanged and further checking is performed as described below. If exactly one conversion is possible, that conversion is applied to the chosen operand and the converted operand is used in place of the original operand for the remainder of this section.

4 If the second and third operands are glvalues of the same value category and have the same type, the result is of that type and value category and it is a bit-field if the second or the third operand is a bit-field, or if both are bit-fields.

5 Otherwise, the result is a prvalue. If the second and third operands do not have the same type, and either has (possibly cv-qualified) class type, overload resolution is used to determine the conversions (if any) to be applied to the operands (13.3.1.2, 13.6). If the overload resolution fails, the program is ill-formed. Otherwise, the conversions thus determined are applied, and the converted operands are used in place of the original operands for the remainder of this section.

6 Lvalue-to-rvalue (4.1), array-to-pointer (4.2), and function-to-pointer (4.3) standard conversions are performed on the second and third operands. After those conversions, one of the following shall hold:

  • The second and third operands have the same type; the result is of that type. If the operands have class type, the result is a prvalue temporary of the result type, which is copy-initialized from either the second operand or the third operand depending on the value of the first operand.
  • The second and third operands have arithmetic or enumeration type; the usual arithmetic conversions are performed to bring them to a common type, and the result is of that type.
  • One or both of the second and third operands have pointer type; pointer conversions (4.10) and qualification conversions (4.4) are performed to bring them to their composite pointer type (Clause 5). The result is of the composite pointer type.
  • One or both of the second and third operands have pointer to member type; pointer to member conversions (4.11) and qualification conversions (4.4) are performed to bring them to their composite pointer type (Clause 5). The result is of the composite pointer type.
  • Both the second and third operands have type std::nullptr_t or one has that type and the other is a null pointer constant. The result is of type std::nullptr_t.

关键的一点是,这将始终尝试转换一个操作数以匹配另一个操作数的类型,而不是将两者都转换为第三种类型,直到到达第 5 段,此时编译器开始查找用户定义的到指针或算术类型的隐式转换(这些只是第 13.6 节中定义的 operator?: 内置候选函数的可能参数),并且出于您的目的,您确实不希望它到达那里。

在最小化示例中,它与您的错误情况直接相关(A = istreamB = ifstream, C = istringstream),将一个类型转换为另一个的类型是不可能的,因此逻辑会下降到 p5,编译器会查找用户定义的隐式转换。在最小化的示例中,没有转换,重载解析失败,并且整个事情的格式不正确。在您的错误情况下,C++11之前的版本(显然在C++11之后的libstdc++中)存在从流到void *的隐式转换,因此编译器会执行此操作,给出整个表达式是 void * 类型,但显然无法绑定(bind)到对 std::istream 的引用,所以这就是您看到的错误。

在第二种情况下:

ifs.is_open() ? ifs : true ? iss : std::cin;

std::cin 具有类型 std::istream,并且 std::istringstream 可以转换为其基类 std::istream,因此内部条件表达式格式良好并且类型为 std::istream。然后,通过外部条件表达式,第二个操作数 std::ifstream 的类型可以转换为第三个操作数 std::istream 的类型,因此整个表达式格式良好,并且具有绑定(bind)到引用的正确类型。

关于c++ - 为什么 std::istringstream 在三元 (?:) 运算符中的解析结果与 std::ifstream 不同?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24706480/

相关文章:

javascript - 重构三元运算符

c++ - char[N] 与 char *

c++ - 我怎样才能 "double overload"一个运算符(operator)?

c++ - 完美转发,为什么析构函数被调用了两次?

C++11:值传递是否涉及 move 语义?

c++ - 可变参数模板函数接受 lambda

javascript - V8 公开类但在特定代码中对其进行限制

c++ - std::enable_if 类型检查

c# - 在条件运算中使用 true 和 false 作为表达式

c++ - 从 x?y :z expression 得到了意想不到的答案