为啥 std::istringstream 在三元 (?:) 运算符中的解析方式似乎与 std::ifstream 不同?

Posted

技术标签:

【中文标题】为啥 std::istringstream 在三元 (?:) 运算符中的解析方式似乎与 std::ifstream 不同?【英文标题】:Why does std::istringstream appear to resolve differently to std::ifstream in the ternary (?:) operator?为什么 std::istringstream 在三元 (?:) 运算符中的解析方式似乎与 std::ifstream 不同? 【发布时间】:2014-09-02 14:17:36 【问题描述】:

我习惯于编写小的命令行工具,它们要么接受文件名,要么从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) 转换为布尔值并获取其 operator 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;

    为什么将std::istringstreamstd::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;
    
    

    为什么这个软糖有效? GCC 是否违反了关于三元运算符 (?:) 如何解析其类型的任何规则?还是我错过了什么?

【问题讨论】:

错误信息与第一个程序不匹配,该代码中没有iss。行号似乎也关闭了,除非您在 main 函数上方有一百多个空行。 @JoachimPileborg:错误消息似乎是针对第二个程序的。 是的,错误消息来自第二个程序。行号是关闭的,因为我从一个更大的程序中删除了所有三个示例。 T.C. 的回答绝对正确。答案相当简单:将三元运算符的一个(或两个)结果转换为std:istream 【参考方案1】:

最小化的例子:

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 否则,如果第二个和第三个操作数的类型不同,并且 要么具有(可能是 cv 限定的)类类型,要么两者都是 glvalues 具有相同的价值类别和相同的类型,除了 cv-qualification,尝试转换每个操作数 到对方的类型。确定是否存在的过程 T1 类型的操作数表达式 E1 可以转换为匹配操作数 T2 类型的表达式 E2 定义如下:

如果 E2 是左值:如果 E1 可以隐式转换(第 4 条)为类型“对 T2 的左值引用”,则可以将 E1 转换为匹配 E2, 受限于在转换中引用必须 直接绑定 (8.5.3) 到左值。 如果 E2 是一个 xvalue:如果 E1 可以隐式转换为“对 T2 的右值引用”类型,则可以将 E1 转换为匹配 E2,受制于 引用必须直接绑定的约束。 如果 E2 是纯右值,或者上述转换均无法完成且至少有一个操作数具有(可能是 cv 限定的) 班级类型: 如果 E1 和 E2 有类类型,并且底层类类型相同或者一个是另一个的基类:E1 可以转换为 如果 T2 的类与以下的类型相同,或者是以下的基类,则匹配 E2 T1 的类别,和 T2 的 cv-qualification 相同 cv-qualification 为或大于 cv-qualification T1 的 cv 资格。如果应用转换,则 E1 更改为 T2 类型的纯右值,通过从 E1 并将该临时值用作转换后的操作数。 否则(即,如果 E1 或 E2 具有非类类型,或者如果它们都具有类类型但基础类不相同或 一个是另一个的基类):如果 E1,E1 可以转换为匹配 E2 可以隐式转换为表达式 E2 将具有的类型 如果 E2 被转换为纯右值(或它具有的类型,如果 E2 是 prvalue)。

利用这个过程,判断第二个操作数是否可以 转换为匹配第三个操作数,以及第三个操作数是否 可以转换为匹配第二个操作数。如果两者都可以 已转换,或者可以转换但转换不明确, 该程序格式不正确。如果两者都不能转换,则操作数 保持不变,并按照所述执行进一步检查 以下。如果恰好可以进行一次转换,则该转换是 应用于所选操作数,转换后的操作数用于 本节剩余部分的原始操作数的位置。

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

5 否则,结果为纯右值。如果第二个和第三个操作数 没有相同的类型,并且有(可能是 cv-qualified) 类类型,重载决议用于确定转换 (如果有)应用于操作数(13.3.1.2、13.6)。如果 重载决议失败,程序格式错误。否则,该 应用如此确定的转换,并且转换后的操作数 用于代替原始操作数的其余部分 部分。

6 左值到右值 (4.1)、数组到指针 (4.2) 和 函数到指针 (4.3) 标准转换是在 第二和第三个操作数。在这些转换之后,其中一个 以下应成立:

第二个和第三个操作数的类型相同;结果就是那种类型。如果操作数具有类类型,则结果是纯右值 结果类型的临时值,它是从任一副本初始化的 第二个操作数或第三个操作数取决于 第一个操作数。 第二个和第三个操作数具有算术或枚举类型;执行通常的算术转换以将它们带到 common 类型,结果就是那个类型。 第二个和第三个操作数中的一个或两个具有指针类型;指针转换 (4.10) 和限定转换 (4.4) 是 执行以将它们带到它们的复合指针类型(第 5 条)。 结果是复合指针类型。 第二个和第三个操作数中的一个或两个都有指向成员类型的指针;指向成员转换(4.11)和资格的指针 执行转换(4.4)以将它们带到它们的复合 指针类型(第 5 条)。结果是复合指针类型。 第二个和第三个操作数都具有std::nullptr_t 类型,或者一个具有该类型而另一个是空指针常量。结果是 类型为std::nullptr_t

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

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

在你的第二种情况下:

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

std::cin 的类型为std::istream,而std::istringstream 可以转换为它的基类std::istream,因此内部条件表达式的格式是良构的,类型为std::istream。然后使用外部条件表达式,再次将第二个操作数 std::ifstream 的类型转换为第三个操作数 std::istream 的类型,因此整个表达式是格式正确的并且具有绑定到参考。

【讨论】:

"5 否则,结果是纯右值" - 所以无论是否形成void * 或其他形式,结果都不能绑定到非 const 左值引用 @MattMcNabb 对。区别仅在于它是否在寻找转换的重载决议中失败,或者在以后尝试绑定时失败。 为什么第三个要点在“如果E2是右值”中使用“右值”而不是“纯右值”,如果某物不是左值或x值,它必须是纯右值。对吗? @0x499602D2 IIRC 我引用了 N3337。由于github.com/cplusplus/draft/issues/55,它在 N3936 中更改为“prvalue”。【参考方案2】:

如果你有一个基类和一个派生类,三元条件运算符知道将派生类转换为基类。但是如果你有两个派生类,它不知道将它们转换为它们的公共基类。这不是 gcc 的行为;这就是标准中指定三元条件运算符的工作方式。

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

这很好用,因为std::cin 的类型为std::istream,它是std::ifstream 的基类。

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

这不起作用,因为 std::ifstreamstd::istringstream "only" 有一个共同的基类。

std::istream& is = ifs.is_open() ? ifs : true ? iss : std::cin; // fudge works

这行得通,因为它被解析为:

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

括号内的表达式的类型为std::istream。所以iss被转换成std::istream类型的左值,如果被选中,ifs也被类似地转换。

【讨论】:

【参考方案3】:

编译器尝试为三元运算符的两个结果找到一个公共类型,如果你看到例如this reference 你会看到有一个casting operator override for void* (or bool for C++11 and later),所以编译器使用它。

但是当它尝试进行分配时,它会出错,因为在初始化的右侧你有一个void*(或者bool)类型,而在左侧有一个引用到std::istream

要解决它,您必须手动将每个流转换为对 std::istream 的引用,例如static_cast.

【讨论】:

好吧,这似乎是合理的。但是为什么它适用于 std::ifstream 和 std::cin 的情况,但对于 std::ifstream 和 std::istringstream 却失败了? @clcto 啊,这很奇怪。我尝试铸造,但它对我不起作用。现在确实如此,所以我在进行该测试时一定犯了另一个错误:) @Galik 为什么它适用于我不知道的“软糖”解决方案,您必须询问编译器开发人员为什么会这样。实际上,您可能希望将第一个程序作为测试用例提供给他们,因为最合乎逻辑的事情是首先使用公共基类。可能有理由让它像规范中那样工作,但我现在已经厌倦了阅读它。 :)【参考方案4】:

gcc 抱怨是因为 ifs 和 iss 是两种不同的类型。将类型静态转换为 std::istream& 将解决您的问题。

【讨论】:

甚至是 static_casting 其中之一;然后另一个将隐式转换。

以上是关于为啥 std::istringstream 在三元 (?:) 运算符中的解析方式似乎与 std::ifstream 不同?的主要内容,如果未能解决你的问题,请参考以下文章

C++ 使用 istringstream 将整数读取为无符号字符

为啥 Go 没有三元条件运算符 [关闭]

为啥三元条件不适用于字符串连接

为啥在三元函数中分配给数组类型的表达式失败

为啥在三元运算符中使用“0”会返回第一个值?

为啥我的三元运算符会出现这些错误?