为啥 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::istringstream
与std::cin
和std::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
= istream
、B
= ifstream
、C
= 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::ifstream
和 std::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 不同?的主要内容,如果未能解决你的问题,请参考以下文章