std::ostringstream打印C字符串的地址而不是其内容

20

我最初遇到了一种我无法解释的奇怪行为(请参见ideone):

#include <iostream>
#include <sstream>
#include <string>

int main() {
  std::cout << "Reference     : "
            << (void const*)"some data"
            << "\n";

  std::ostringstream s;
  s << "some data";
  std::cout << "Regular Syntax: " << s.str() << "\n";

  std::ostringstream s2;
  std::cout << "Semi inline   : "
            << static_cast<std::ostringstream&>(s2 << "some data").str()
            << "\n";

  std::cout << "Inline        : "
            << dynamic_cast<std::ostringstream&>(
                 std::ostringstream() << "some data"
               ).str()
            << "\n";
}

输出结果:

Reference     : 0x804a03d
Regular Syntax: some data
Semi inline   : some data
Inline        : 0x804a03d

令人惊讶的是,在最后一个播放中,我们得到的是地址而不是内容!

为什么会这样呢?

3个回答

20

std::ostringstream()表达式创建了一个临时对象,而接受const char*参数的operator<<是一个自由函数,但这个自由函数不能在临时对象上调用,因为该函数的第一个参数类型为std::ostream&,无法与临时对象绑定。

话虽如此,<<std::ostringstream() << "some data"会解析为调用一个成员函数,该成员函数对void*进行了重载,以打印地址。请注意,成员函数可以在临时对象上调用。

为了调用自由函数,您需要将临时对象(即右值)转换为左值,并且这里有一个可以使用的技巧:

 std::cout << "Inline        : "
            << dynamic_cast<std::ostringstream&>(
                 std::ostringstream().flush() << "some data"
               ).str()
            << "\n";

也就是说,std::ostringstream().flush() 返回一个 std::ostream& 引用,这意味着现在可以调用该自由函数,并将其返回的引用作为第一个参数传递。

此外,在这里你不需要使用 dynamic_cast(会降低速度,因为它是在运行时完成的),因为对象的类型是已知的,所以你可以使用 static_cast(速度快,因为它是在编译时完成的):

 std::cout << "Inline        : "
            << static_cast<std::ostringstream&>(
                 std::ostringstream().flush() << "some data"
               ).str()
            << "\n";

这应该能正常工作。


2
没错!我花了一个小时才弄明白 :x 我在下面发布了我的调查结果...不知怎么的,我希望我早点问(尽管这样不会那么有教育意义)。 - Matthieu M.
3
哇!你的解决方法比我的更整洁。绝对是那种让我想砸 Stroustrup 的头,因为他排除了非 const 绑定的日子。 :x - Matthieu M.
@MatthieuM:Kerrek和我就此进行了小讨论。如果您感兴趣,请在此处阅读评论(我的回答下的评论)。还有其他解决方法,但使用flush比其他方法更好。 - Nawaz
我不理解这行代码,因为函数的第一个参数类型是std::ostream&,它不能绑定到临时对象。你能用简单的话来表达这句话吗? - Mr.Anubis
2
@FreakEnum:你不能写成这样:A & a = A(),因为表达式A()创建了一个类型为A的临时对象,但是根据语言规范,非const引用(在赋值的左侧)无法绑定到临时对象。但是一旦将其变为const引用,则绑定就成为可能;也就是说,你可以写成这样:A const & a = A()。同样地,std::ostream&是一个非const引用,无法绑定到由表达式std::ostringstream()创建的临时对象。 - Nawaz
@FreakEnum:更多细节可以阅读此主题:C++中构造函数中的临时非const istream引用 - Nawaz

9

一个临时变量不能绑定到非const形式参数的引用上。

因此,非成员<<不会被使用。

你会得到void*版本。

C++11通过添加一个非成员rvalue流插入器函数来修复这个问题,

C++11
§27.7.3.9 Rvalue stream insertion
[ostream.rvalue]
template <class charT, class traits, class T>
basic_ostream<charT, traits>&
operator<<(basic_ostream<charT, traits>&& os, const T& x);

1 效果: os << x
2 返回: os


啊,谢谢你指出这个,我不知道他们在C++11中已经解决了这个问题。通过添加右值引用/移动语义,简化生活真是太神奇了。 - Matthieu M.
@Matthieu:不幸的是,由于这些问题,其他微妙的问题也出现了,似乎对于这些讨厌的临时流没有真正令人满意的解决方案。 - Xeo
@Xeo:是的,我已经看到了(并点赞了):) 我真的很喜欢Howard提供的缺陷链接(至少是它提出的解决方案)。 - Matthieu M.
添加一个包含讨论1203. 更有用的右值流插入的参考。 - samm

4
为了开始,最简单的解决方案是获取编译器考虑的可能重载列表,例如尝试这样做:
X x;
std::cout << x << "\n";

其中X是一种没有任何流重载的类型,它产生以下可能的重载列表:

prog.cpp: In function ‘int main()’:
prog.cpp:21: error: no match for ‘operator<<’ in ‘std::cout << x’
include/ostream:112: note: candidates are: std::ostream& std::ostream::operator<<(std::ostream& (*)(std::ostream&))
include/ostream:121: note:                 std::ostream& std::ostream::operator<<(std::basic_ios<_CharT, _Traits>& (*)(std::basic_ios<_CharT, _Traits>&))
include/ostream:131: note:                 std::ostream& std::ostream::operator<<(std::ios_base& (*)(std::ios_base&))
include/ostream:169: note:                 std::ostream& std::ostream::operator<<(long int)
include/ostream:173: note:                 std::ostream& std::ostream::operator<<(long unsigned int)
include/ostream:177: note:                 std::ostream& std::ostream::operator<<(bool)
include/bits/ostream.tcc:97: note:         std::ostream& std::ostream::operator<<(short int)
include/ostream:184: note:                 std::ostream& std::ostream::operator<<(short unsigned int)
include/bits/ostream.tcc:111: note:        std::ostream& std::ostream::operator<<(int)
include/ostream:195: note:                 std::ostream& std::ostream::operator<<(unsigned int)
include/ostream:204: note:                 std::ostream& std::ostream::operator<<(long long int)
include/ostream:208: note:                 std::ostream& std::ostream::operator<<(long long unsigned int)
include/ostream:213: note:                 std::ostream& std::ostream::operator<<(double)
include/ostream:217: note:                 std::ostream& std::ostream::operator<<(float)
include/ostream:225: note:                 std::ostream& std::ostream::operator<<(long double)
include/ostream:229: note:                 std::ostream& std::ostream::operator<<(const void*)
include/bits/ostream.tcc:125: note:        std::ostream& std::ostream::operator<<(std::basic_streambuf<_CharT, _Traits>*)

浏览这个列表,我们可以注意到char const*显然缺失了,因此选择void const*并打印地址是合理的。
仔细看一下,我们注意到所有重载都是方法,没有一个自由函数出现在这里。
问题是引用绑定的问题:因为临时对象不能绑定到非常量引用,所以形式为std::ostream& operator<<(std::ostream&,X)的重载被直接拒绝,只剩下成员函数。
就我而言,这是C++中的设计缺陷,毕竟我们正在对临时对象执行可变成员函数,并且这需要对对象进行(隐藏的)引用 :x
一旦你理解了出了什么问题,解决方法相对简单,只需要一个小包装器。
struct Streamliner {
  template <typename T>
  Streamliner& operator<<(T const& t) {
    _stream << t;
    return *this;
  }

  std::string str() const { return _stream.str(); }
  std::ostringstream _stream;
};

std::cout << "Inline, take 2: " << (Streamliner() << "some data").str() << "\n";

输出预期结果。


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