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

pb3skfrl  于 2024-01-09  发布在  其他
关注(0)|答案(4)|浏览(107)

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

  1. int main(int argc, char* argv[])
  2. {
  3. std::string filename;
  4. // args processing ...
  5. std::ifstream ifs;
  6. if(!filename.empty())
  7. ifs.open(filename);
  8. std::istream& is = ifs.is_open() ? ifs : std::cin;
  9. std::string line;
  10. while(std::getline(is, line))
  11. {
  12. // process line...
  13. }
  14. return 0;
  15. }

字符串
在阅读了一个关于Stack Overflow的问题后,我试图修改我常用的模式,以适应从文件或std::istringstream读取的需要。令我惊讶的是,它无法编译并给出以下错误:

  1. 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*’
  2. std::istream& is = ifs.is_open() ? ifs : iss; // won't compile


在我看来,它试图将std::istringstream对象(iss)转换为布尔值并获得其operator void*()

  1. int main(int argc, char* argv[])
  2. {
  3. std::string filename;
  4. std::string data;
  5. // args processing ...
  6. std::ifstream ifs;
  7. std::istringstream iss;
  8. if(!filename.empty())
  9. ifs.open(filename);
  10. std::istream& is = ifs.is_open() ? ifs : iss; // won't compile
  11. std::string line;
  12. while(std::getline(is, line))
  13. {
  14. // process line...
  15. }
  16. return 0;
  17. }


1.为什么std::istringstreamstd::cinstd::ifstream有区别?它们都是从std::istream派生而来的。
然后我记得我已经转换了我的模式以适应三种可能性,从文件、字符串或std::cin中阅读。我记得这是有效的(尽管它相当笨拙)。所以应用三重解决方案来解决这个问题,我想出了一个完全有效的软糖:

  1. int main(int argc, char* argv[])
  2. {
  3. std::string filename;
  4. std::string data;
  5. // args processing ...
  6. std::ifstream ifs;
  7. std::istringstream iss;
  8. if(!filename.empty())
  9. ifs.open(filename);
  10. std::istream& is = ifs.is_open() ? ifs : true ? iss : std::cin; // fudge works
  11. std::string line;
  12. while(std::getline(is, line))
  13. {
  14. // process line...
  15. }
  16. return 0;
  17. }


1.为什么这个捏造的工作?GCC是否违反了三元运算符(?:)如何解析其类型的规则?或者我错过了什么?

xesrikrc

xesrikrc1#

最小化示例:

  1. class A { };
  2. class B : public A { };
  3. class C : public A { };
  4. int main() {
  5. B b;
  6. C c;
  7. A& refA = true? b : c;
  8. }

字符串
Clang报告:

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


相关规则见标准§5.16 [expr.cond]/p3-6:
3否则,如果第二个和第三个操作数具有不同的类型,(可能是cv限定的)类类型,或者如果两者都是相同值类别和相同类型的glvalues,除了cv限定,用于确定类型T1的操作数表达式E1是否可以被转换以匹配操作数表达式E2的过程类型T2的定义如下:

  • 如果E2是左值:如果E1可以隐式地转换(第4节)为类型“lvalue reference to T2”,则E1可以被转换为匹配E2,条件是在转换中引用必须直接绑定(8.5.3)到lvalue。
  • 如果E2是一个xvalue:如果E1可以隐式转换为“对T2的右值引用”类型,则E1可以转换为与E2匹配,但必须遵守引用必须直接绑定的约束。
  • 如果E2是纯右值,或者上面的转换都不能完成,并且至少有一个操作数具有(可能是cv限定的)类类型:
  • 如果E1和E2都有类类型,并且底层类类型相同或者一个是另一个的基类:如果T2的类与T1的类是相同的类型或者是T1的类的基类,并且T2的cv限定与T1的cv限定是相同的cv限定或者是比T1的cv限定更大的cv限定,则可以将E1转换为匹配E2。如果应用转换,通过从E1复制初始化T2类型的临时值并将该临时值用作转换后的操作数,将E1更改为T2类型的纯右值。
  • 否则(即,如果E1或E2具有非类类型,或者如果它们都具有类类型,但底层类既不相同,也不是另一个的基类):如果E1可以隐式转换为表达式E2在E2转换为纯右值时所具有的类型(或者如果E2是纯右值,则其具有的类型),则E1可以转换为匹配E2。

使用此过程,确定第二个操作数是否可以转换为与第三个操作数匹配,以及第三个操作数是否可以转换为与第二个操作数匹配。如果两者都可以转换,或者一个可以转换但转换不明确,则程序是病态的。如果两者都不能转换,操作数保持不变并且如下所述执行进一步的检查。如果恰好一个转换是可能的,该转换被应用于所选择的操作数,并且对于本部分的剩余部分,使用转换后的操作数来代替原始操作数。
4如果第二个和第三个操作数是相同值类别的glvalues,并且具有相同的类型,则结果是该类型和值类别,并且如果第二个或第三个操作数是位字段,或者如果两者都是位字段,则它是位字段。
5否则,结果是纯右值。如果第二个和第三个操作数的类型不同,并且其中一个具有(可能是cv限定的)类类型,重载解析用于确定要应用于操作数的转换(如果有的话)(13.3.1.2,13.6).如果重载解析失败,则程序是病态的。否则,应用由此确定的转换,并且对于本节的剩余部分,使用转换后的操作数来代替原始操作数。
6左值到右值(4.1)、数组到指针(4.2)和函数到指针(4.3)的标准转换是在第二个和第三个操作数上执行的。在这些转换之后,以下之一应该成立:

  • 第二个和第三个操作数具有相同的类型;结果也是该类型。如果操作数具有类类型,则结果是结果类型的纯右值临时值,根据第一个操作数的值,从第二个操作数或第三个操作数复制初始化。
  • 第二个和第三个操作数具有算术或枚举类型;执行通常的算术转换以将它们转换为公共类型,并且结果是该类型。
  • 第二个和第三个操作数中的一个或两个都有指针类型;执行指针转换(4.10)和限定转换(4.4)将它们转换为复合指针类型(第5章)。结果是复合指针类型。
  • 第二个和第三个操作数中的一个或两个都有指向成员类型的指针;执行指向成员类型的指针转换(4.11)和限定转换(4.4)以将它们转换为复合指针类型(第5章)。结果是复合指针类型。
  • 第二个和第三个操作数都是std::nullptr_t类型,或者一个是std::nullptr_t类型,另一个是空指针常量。结果是std::nullptr_t类型。

关键的一点是,这将始终尝试转换一个操作数以匹配另一个操作数的类型,而不是将两者都转换为第三种类型,直到您遇到第5段,此时编译器开始查找用户定义的指针或算术类型的隐式转换(这些只是第13.6节中定义的operator?:的内置候选函数的可能参数),对于您的目的,您真的不希望它到达那里。
在最小化的示例中,它与错误情况直接相关(A = istreamB = ifstreamC = istringstream),将一个转换为另一个的类型是不可能的,因此逻辑下降到p5,编译器寻找用户定义的隐式转换。在最小化的示例中没有转换,重载解析失败,整个事情都是病态的。在你的错误情况下,C11之前(显然,在libstdc post-C++11中)有一个从流到void *的隐式转换,所以编译器这样做,给整个表达式一个void *类型,但显然不能绑定到std::istream的引用,这就是你看到的错误。
在你的第二个案例中:

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


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

展开查看全部
evrscar2

evrscar22#

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

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

字符串
这很好,因为std::cinstd::istream类型,它是std::ifstream的基类。

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


这不起作用,因为std::ifstreamstd::istringstream“只有”一个公共基类。

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


这是有效的,因为它被解析为:

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


带括号的表达式的类型为std::istream。因此,如果选择iss,则将其转换为std::istream类型的左值,ifs也将进行类似的转换。

展开查看全部
jvlzgdj9

jvlzgdj93#

编译器试图为三进制运算符的两个结果找到一个公共类型,如果你看到例如this reference,你会看到有一个casting operator override for void* (or bool for C++11 and later),所以编译器使用它。
但是当它尝试赋值时,它出错了,因为在初始化的右边有一个void*(或者bool)类型,而在左边有一个对std::istream的引用。
为了解决这个问题,你必须手动将每个流转换为std::istream的引用,例如static_cast

szqfcxe2

szqfcxe24#

gcc正在抱怨,因为ifs和iss是两种不同的类型。static_casting类型到std::istream&将解决您的问题。

相关问题