【问题标题】:Overload on ostream in a variadic template function可变参数模板函数中的 ostream 重载
【发布时间】:2011-05-13 20:21:03
【问题描述】:

我有一个可变参数函数,我想在第一个参数类型上重载。

void write( void ) { }

void write( std::ostream& ) { }

template< typename Head, typename... Rest >
void write( std::ostream& out, Head&& head, Rest&&... rest )
{
   out << head;
   write( out, std::forward<Rest>(rest)... );
}

template< typename... Args >
void write( Args&&... args )
{
   write( std::cout, std::forward<Args>(args)... );
}

但是这些函数的行为并不像预期的那样。

write( "## to cout ##" ); // printed to stdout as expected
write( std::cerr, "## to cerr ##" ); // printed to stderr as expected
std::ostringstream oss;
write( oss, "## to string ##" );  // error here
// '0x7fff9db8## to string ##' is printed to stdout!

这是怎么回事?
为什么重载分辨率不选择我想要的功能?
有没有办法在没有大量元编程的情况下做到这一点? (我可以使用 std::is_convertible 解决它,但解决方案比我上面显示的简单代码大得多)。

【问题讨论】:

  • 为什么需要最后一个模板特化?没有那个问题就会消失。
  • @Diego,如果调用者未提供 ostream 对象,则最后一个特化 (write(args)) 将 std::cout 作为第一个参数。

标签: c++ c++11 variadic-templates overload-resolution


【解决方案1】:

这是因为ostringstream 在您将其传递给另一个模板时需要基本转换为ostream,而当您将其传递给转发给write(std::cout, ...) 的模板时则不需要任何转换。因此,如果您传递ostringstream,它会选择更通用的模板,该模板将 ostringstream 作为参数转发到更具体的模板。输出 ostringstream 会将其转换为 void*,然后打印出来。

您可以使用is_base_of 解决这个问题(我感觉比使用is_convertible 更好)。

template<typename Arg, typename... Args, typename =
  typename std::enable_if<
    !std::is_base_of<
      std::ostream,
      typename std::remove_reference<Arg>::type, 
      >::value>::type
>
void write(Arg&& arg, Args&&... args )
{
   write( std::cout, std::forward<Arg>(arg), std::forward<Args>(args)... );
}

我个人不喜欢在我的代码中使用过多的 SFINAE,因为我无法处理一定程度的尖括号。所以我喜欢使用重载

template< typename Arg, typename... Args >
void write_dispatch( std::true_type, Arg&& arg, Args&&... args )
{
   std::ostream& os = arg;
   write( os, std::forward<Args>(args)... );
}

template< typename Arg, typename... Args >
void write_dispatch( std::false_type, Arg&& arg, Args&&... args )
{
   write( std::cout, std::forward<Arg>(arg), std::forward<Args>(args)... );
}

template< typename Arg, typename... Args >
void write( Arg&& arg, Args&&... args )
{
   typedef typename std::remove_reference<Arg>::type nonref_type;
   write_dispatch( std::is_base_of<std::ostream, nonref_type>(), 
          std::forward<Arg>(arg), std::forward<Args>(args)... );
}

这样,如果你用不是ostream的左值作为第一个参数来调用它,它将调用write_dispatch,这会将调用转换为ostream这样的左值,这样你的另一个@987654334 @template 可以继续。

最后一点,你应该说out &lt;&lt; std::forward&lt;Head&gt;(head),否则你在前面的递归步骤中使用std::forward 的所有工作都是徒劳的,因为最终你会以左值的形式输出所有内容。

【讨论】:

  • 所以你是说重载解析更喜欢 T&& 而不是基类转换?有没有办法可以调整签名来避免这种情况?
  • @deft_code,我想你已经知道了,你可以用is_convertible&lt;T, std::ostream&amp;&gt;is_base_of&lt;std::ostream, T&gt; 来做到这一点。
  • @litb,当 T 私下继承自 std::ostream 时,is_base_of 不也是真的吗?我不知道为什么有人会这样做,但这会导致示例中的static_cast 无法编译。
  • @deft 正确。我想这是一个语义问题。我不介意私人派生自ostream 的类在与write 一起使用时无法编译。如果通过了 const 流,write_dispatch 将失败。我会在那里使用 c 风格的演员表,它会抛弃 const 并且会默默地将它作为非常量流转发。我认为std::is_convertible&lt;Arg, std::ostream&amp;&gt; 也可以,也适用于struct A { operator std::ostream&amp;(); }; 之类的东西。我认为是否将A 视为流是一个品味问题。我不会。
  • @litb,我在想象自定义类从ostream 私下派生但也提供流插入运算符的病态情况。我想用户会希望课程像std::cout 一样流出。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-12-23
  • 1970-01-01
  • 2019-01-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多