通过隐式转换为字符串流对象时,出现了过载解析失败

22

免责声明: 我知道应该避免将变量隐式转换为字符串,并且正确的方法是对 Person 进行 op<< 重载。


请考虑以下代码:

#include <string>
#include <ostream>
#include <iostream>

struct NameType {
   operator std::string() { return "wobble"; }
};

struct Person {
   NameType name;
};

int main() {
   std::cout << std::string("bobble");
   std::cout << "wibble";

   Person p;
   std::cout << p.name;
}

通过GCC 4.3.4,它产生了以下结果

prog.cpp: In function ‘int main()’:
prog.cpp:18: error: no match for ‘operator<<’ in ‘std::cout << p.Person::name’
/usr/lib/gcc/i686-pc-linux-gnu/4.3.4/include/g++-v4/ostream:112: note: candidates are: std::basic_ostream<_CharT, _Traits>& std::basic_ostream<_CharT, _Traits>::operator<<(std::basic_ostream<_CharT, _Traits>& (*)(std::basic_ostream<_CharT, _Traits>&)) [with _CharT = char, _Traits = std::char_traits<char>]
/usr/lib/gcc/i686-pc-linux-gnu/4.3.4/include/g++-v4/ostream:121: note:                 std::basic_ostream<_CharT, _Traits>& std::basic_ostream<_CharT, _Traits>::operator<<(std::basic_ios<_CharT, _Traits>& (*)(std::basic_ios<_CharT, _Traits>&)) [with _CharT = char, _Traits = std::char_traits<char>]
/usr/lib/gcc/i686-pc-linux-gnu/4.3.4/include/g++-v4/ostream:131: note:                 std::basic_ostream<_CharT, _Traits>& std::basic_ostream<_CharT, _Traits>::operator<<(std::ios_base& (*)(std::ios_base&)) [with _CharT = char, _Traits = std::char_traits<char>]
/usr/lib/gcc/i686-pc-linux-gnu/4.3.4/include/g++-v4/ostream:169: note:                 std::basic_ostream<_CharT, _Traits>& std::basic_ostream<_CharT, _Traits>::operator<<(long int) [with _CharT = char, _Traits = std::char_traits<char>]
/usr/lib/gcc/i686-pc-linux-gnu/4.3.4/include/g++-v4/ostream:173: note:                 std::basic_ostream<_CharT, _Traits>& std::basic_ostream<_CharT, _Traits>::operator<<(long unsigned int) [with _CharT = char, _Traits = std::char_traits<char>]
/usr/lib/gcc/i686-pc-linux-gnu/4.3.4/include/g++-v4/ostream:177: note:                 std::basic_ostream<_CharT, _Traits>& std::basic_ostream<_CharT, _Traits>::operator<<(bool) [with _CharT = char, _Traits = std::char_traits<char>]
/usr/lib/gcc/i686-pc-linux-gnu/4.3.4/include/g++-v4/bits/ostream.tcc:97: note:                 std::basic_ostream<_CharT, _Traits>& std::basic_ostream<_CharT, _Traits>::operator<<(short int) [with _CharT = char, _Traits = std::char_traits<char>]
/usr/lib/gcc/i686-pc-linux-gnu/4.3.4/include/g++-v4/ostream:184: note:                 std::basic_ostream<_CharT, _Traits>& std::basic_ostream<_CharT, _Traits>::operator<<(short unsigned int) [with _CharT = char, _Traits = std::char_traits<char>]
/usr/lib/gcc/i686-pc-linux-gnu/4.3.4/include/g++-v4/bits/ostream.tcc:111: note:                 std::basic_ostream<_CharT, _Traits>& std::basic_ostream<_CharT, _Traits>::operator<<(int) [with _CharT = char, _Traits = std::char_traits<char>]
/usr/lib/gcc/i686-pc-linux-gnu/4.3.4/include/g++-v4/ostream:195: note:                 std::basic_ostream<_CharT, _Traits>& std::basic_ostream<_CharT, _Traits>::operator<<(unsigned int) [with _CharT = char, _Traits = std::char_traits<char>]
/usr/lib/gcc/i686-pc-linux-gnu/4.3.4/include/g++-v4/ostream:204: note:                 std::basic_ostream<_CharT, _Traits>& std::basic_ostream<_CharT, _Traits>::operator<<(long long int) [with _CharT = char, _Traits = std::char_traits<char>]
/usr/lib/gcc/i686-pc-linux-gnu/4.3.4/include/g++-v4/ostream:208: note:                 std::basic_ostream<_CharT, _Traits>& std::basic_ostream<_CharT, _Traits>::operator<<(long long unsigned int) [with _CharT = char, _Traits = std::char_traits<char>]
/usr/lib/gcc/i686-pc-linux-gnu/4.3.4/include/g++-v4/ostream:213: note:                 std::basic_ostream<_CharT, _Traits>& std::basic_ostream<_CharT, _Traits>::operator<<(double) [with _CharT = char, _Traits = std::char_traits<char>]
/usr/lib/gcc/i686-pc-linux-gnu/4.3.4/include/g++-v4/ostream:217: note:                 std::basic_ostream<_CharT, _Traits>& std::basic_ostream<_CharT, _Traits>::operator<<(float) [with _CharT = char, _Traits = std::char_traits<char>]
/usr/lib/gcc/i686-pc-linux-gnu/4.3.4/include/g++-v4/ostream:225: note:                 std::basic_ostream<_CharT, _Traits>& std::basic_ostream<_CharT, _Traits>::operator<<(long double) [with _CharT = char, _Traits = std::char_traits<char>]
/usr/lib/gcc/i686-pc-linux-gnu/4.3.4/include/g++-v4/ostream:229: note:                 std::basic_ostream<_CharT, _Traits>& std::basic_ostream<_CharT, _Traits>::operator<<(const void*) [with _CharT = char, _Traits = std::char_traits<char>]
/usr/lib/gcc/i686-pc-linux-gnu/4.3.4/include/g++-v4/bits/ostream.tcc:125: note:                 std::basic_ostream<_CharT, _Traits>& std::basic_ostream<_CharT, _Traits>::operator<<(std::basic_streambuf<_CharT, _Traits>*) [with _CharT = char, _Traits = std::char_traits<char>]

为什么自由的op<<(ostream&, string const&)不会进入重载集?这是因为期望的重载是模板实例化和... ADL的组合吗?


可能是重复的问题:为什么编译器不执行类型转换? - Armen Tsirunyan
@Armen:乍一看可能是相同的问题,但回答表明实际上是不同的问题。也许。 - Lightness Races in Orbit
即使它能工作,这也是糟糕的设计。想象一下,如果您还将NameType隐式转换为int,那会怎样。(这可能没有意义,但您可以想象一个具有几个合理的隐式转换运算符的类。)现在,cout << p.name变得模棱两可。 - japreiss
@japreiss:朋友,请阅读问题的第一行。 - Lightness Races in Orbit
我的意思是,在流IO中使用隐式转换是不好的设计,而不是一开始就没有它们。 - japreiss
@japreiss:这两种方式都不太好。 - Lightness Races in Orbit
5个回答

22

C++98中14.8.1/4的规定:

如果函数参数类型不包含参与模板参数推导的template-parameters,则将对函数参数进行隐式转换(在第4条款中)。以将其转换为相应函数参数的类型。

这里您想要一个实例化:

template <class charT, class traits, class Allocator>
  basic_ostream<charT, traits>&
    operator<<(basic_ostream<charT, traits>&,
               const basic_string<charT, traits, Allocator>&);

在不显式提供任何模板参数的情况下进行推导。因此,所有参数都包含一个模板参数,这些参数参与了模板参数推导,因此它们都不能从隐式转换中获得其值。


1
这种语言真难懂。所以,“参数类型”在这里是std::basic_string<charT, traits, Allocator>(针对某些特殊情况),而这种类型当然包含“模板参数”,因为我依赖于推断。唉! - Lightness Races in Orbit

8
因为它是一个模板。要使其生效,您需要先实例化该模板,然后再使用转换运算符。由于顺序错误,所以无法正常工作。
无论您之前在程序中是否使用了特定的运算符,都没有关系。每次使用都被视为单独的。
被视为候选项的重载是那些可以从std::ostream推导出所有模板参数或那些是该类的成员的重载。
如果我们添加一个非模板运算符会怎样呢?
#include <string> 
#include <ostream> 
#include <iostream>  

struct NameType {
   operator std::string() { return "wobble"; } 
};  

struct Person {
    NameType name;
};  

void operator<<(std::ostream& os, const std::string& s)   // ** added **
{ std::operator<<(os, s); }

int main() 
{    
    std::cout << std::string("bobble");
    std::cout << "wibble";

     Person p;
     std::cout << p.name; 
}  

现在它可以正常工作,并输出结果。
 bobblewibblewobble

2
它是由int main()中的第一行实例化的。不是吗? - Lightness Races in Orbit
1
感谢您的编辑。但我仍不太确定。以前的模板实例化应该已经产生了一个可行的候选项(它接受std::string)。您的示例中的变化涉及名称空间以及非模板性,这符合Nawaz令人信服的ADL理论。 - Lightness Races in Orbit
这是之前搜索过的相同命名空间,即coutNameType。我在这里展示的是,当候选项是非模板时,用户定义的转换运算符被使用,但当输出运算符是模板时,它甚至不是候选项。 - Bo Persson
1
如果模板被一个不相关的早期使用实例化,从而改变了重载分辨率的候选集,那将是非常糟糕的。突然删除一些实例化所需模板的死代码可能会导致大量令人困惑的错误消息。 - bames53
为了准确性,operator<< 应该返回 std::ostream&(实际上是 std::basic_ostream)。 - Andriy Tylychko
@Andy - 对于一般情况,这是正确的,可以启用链接,但在这个例子中并不重要。 - Bo Persson

2
由于用户定义的转换函数没有在 ADL 中考虑到。ADL 意味着重载集包含来自参数所定义的命名空间中的重载函数。这里 operator<< 的参数类型是 NameType,但是在定义 NameType 的命名空间中没有定义 operator<<(std::ostream&, const NameType&)。因此出现了错误,因为搜索适当的重载就在那里停止了。这就是 ADL。ADL 不会进一步查看 NameType 的定义以确定它是否定义了任何用户定义的转换函数。
如果您执行以下操作,将会得到 相同的错误
NameType name;
std::cout << name ; //error: user-defined conversion not considered.

你需要投掷它:
std::cout << (std::string)name << std::endl; //ok - use std::string()

此外,您可能会有多个用户定义的转换函数:
std::cout << (int)name << std::endl; //ok - use int() instead

输出在 ideone 上:
wobble
100

1
你能解释一下在这里ADL是如何应用的,以及为什么用户自定义转换没有被考虑吗? - Lightness Races in Orbit
@Tomalak:添加了解释。 :-) - Nawaz
1
等等,为什么不考虑std呢?LHS(std::cout)应该通过ADL将其引入重载集吧? - Lightness Races in Orbit
@Tomalak:用户定义的转换不在考虑范围内。这里适用的是ADL。由于参数的类型是NameType,因此它搜索了未定义的operator << (std::ostream&,const NameType&)。它不会搜索operator<<std::string版本。 - Nawaz
@Tomalak:考虑使用std,但即使如此,它也没有适当的重载。 - Nawaz
显示剩余4条评论

0

只有在以下情况下才会调用转换为字符串:

a) 明确请求 (string) p.name

b) 分配给字符串 string a = p.name

c) ...

如果当前情况不符合任何情况,则可以通过至少两种方式强制调用 ostream<<(ostream&,string)

  1. http://ideone.com/SJe5W 使 NameType 成为字符串(通过公共继承)。

  2. 转到情况 a):如示例中转换为 (int),明确请求转换。

我真的更喜欢选项 1


-3
这是因为用户定义的转换无法链接。举个例子来解释:
struct A {
  void operator = (const int i);
};

struct B {
  operator int ();
}

A a;
B b;
a = b;  // error! because, compiler will not match "A::operator=" and "B::operator int"

这里是我之前提出的类似问题

在你的情况下,你的第一个用户定义转换是:

(1) NameType::operator std::string()

(2) operator <<(ostream&, const std::string&),有点像ostream::operator<<(std::string&)

当你写cout << p.name;时,现在有两种对象面对面:

ostream (LHS) <====> NameType (RHS)

现在,只有当RHS是string时才会调用operator <<(ostream&, const string&)。但这里是NameType,所以它不会被调用。

而且,只有当LHS是string时才会调用NameType::operator string ()。但这里是ostream,所以它不会被调用。

为了使这个等式成立,编译器应该调用上述两种运算符方法中的任何一种。但C++不支持这样做。为什么不支持,在我上面发布的链接中有描述。


2
所以在“op string”中有一个用户定义的转换,但另一个在哪里? - Lightness Races in Orbit
我不理解函数是如何进行转换的。 - Lightness Races in Orbit
@Tomalak,operator<<()是一个函数,在其中你试图接收const std::string&;除非传递了显式的(const) std::string&,否则永远不会匹配。你正在传递NameType [NameType::string operator()不被考虑,如果LHS具有非std::string(这里是ostream)类型]。尝试将NameType强制转换为std::string并传递,然后它将起作用,因为现在你明确地传递了std::string - iammilind
s/string*/string&/,猜测是这样吗? - Lightness Races in Orbit
2
除了完全不等于。 - Lightness Races in Orbit
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接