【问题标题】:How to write a streaming 'operator<<' that can take arbitary containers (of type 'X')?如何编写可以采用任意容器(类型为'X')的流式'operator<<'?
【发布时间】:2012-11-23 08:51:54
【问题描述】:

我有一个 C++ 类“X”,如果将其中的 一个容器 发送到 std::ostream,它将具有特殊含义。

我最初是专门为std::vector&lt;X&gt;实现的:

std::ostream& operator << ( std::ostream &os, const std::vector<X> &c )
{
   // The specialized logic here expects c to be a "container" in simple
   // terms - only that c.begin() and c.end() return input iterators to X
}

如果我想支持std::ostream &lt;&lt; std::deque&lt;X&gt;std::ostream &lt;&lt; std::set&lt;X&gt; 或任何类似的容器类型,我知道的唯一解决方案是复制粘贴整个函数并仅更改函数签名!

有没有办法对operator &lt;&lt; ( std::ostream &amp;, const Container &amp; ) 进行通用编码?

("Container" 这里是任何满足上面注释描述的类型。)

【问题讨论】:

  • 我会用一个模板来回答这个问题,该模板只是在容器上循环打印每个项目,但是你写了“操作会比单独发送每个 X 更复杂”,我认为需要更多解释你真正想要它做什么?
  • 只需为Container 重载operator&lt;&lt;(这真的是容器类的类型吗?还是更像Container&lt;T&gt;?)。如果类中包含的项目有输出函数,C++ 中没有任何魔法可以为容器类生成输出。
  • 我有兴趣回答@J99 的第一个问题。你能向我们展示一下operator&lt;&lt; 在矢量情况下的实现吗?
  • 一个非常简单的解决方案是使用ostream &lt;&lt; describe_range(your_container); 并让describe_range 返回std::stringoperator&lt;&lt;(ostream&amp;, range_description&lt;Cont&gt;) 上的重载或类似的东西。
  • 实际上有一种相当简单的方法可以使“X 容器上的过载”工作,我基本上已经完成了,但我仍在尝试一些事情我在答案草稿中保存的解释消失了,所以是的......也许今晚晚些时候。

标签: c++ templates stl iostream


【解决方案1】:

如果您之前阅读过此答案,您可能需要向下滚动到下面的 ADL 版本。进步了很多。

首先,一个简短而实用的版本:

#include <iostream>
#include <type_traits>
template<typename T, typename Iterator, typename=void>
struct is_iterator_of_type: std::false_type {};

template<typename T, typename Iterator>
struct is_iterator_of_type<
  T,
  Iterator,
  typename std::enable_if<
    std::is_same<
      T,
      typename std::iterator_traits< Iterator >::value_type
    >::value
  >::type
>: std::true_type {};

template<typename Container>
auto operator<<( std::ostream& stream, Container const& c ) ->
  typename std::enable_if< is_iterator_of_type<int, typename Container::iterator>::value, std::ostream& >::type
{
  return stream << "int container\n";
}
template<typename Container>
auto operator<<( std::ostream& stream, Container const& c ) ->
  typename std::enable_if< is_iterator_of_type<double, typename Container::iterator>::value, std::ostream& >::type
{
  return stream << "double container\n";
}

它只检测看起来有点像 intdouble 具有明显重载的容器的东西。我建议更改operator&lt;&lt; 的实现。 ;)

更合适的路线(感谢@Xeo)是这个 adl-hack。我们创建了一个辅助命名空间,我们从std 导入beginend,然后一些模板函数在beginend 上进行参数相关查找(如果没有,请查看std 版本一个更紧密的绑定),然后使用这些aux::adl_begin 函数来确定我们传入的内容是否可以视为 X 上的容器:

#include <iostream>
#include <vector>
#include <type_traits>
#include <iterator>
#include <set>

template<typename T, typename Iterator, typename=void>
struct is_iterator_of_type: std::false_type {};

template<typename T, typename Iterator>
struct is_iterator_of_type<
  T,
  Iterator,
  typename std::enable_if<
    std::is_same<
      T,
      typename std::iterator_traits< Iterator >::value_type
    >::value
  >::type
>: std::true_type {};

namespace aux {
  using std::begin;
  using std::end;
  template<class T>
  auto adl_begin(T&& v) -> decltype(begin(std::forward<T>(v))); // no implementation
  template<class T>
  auto adl_end(T&& v) -> decltype(end(std::forward<T>(v))); // no implementation
}

template<typename T, typename Container, typename=void>
struct is_container_of_type: std::false_type {};

template<typename T, typename Container>
struct is_container_of_type<
  T,
  Container,
  typename std::enable_if<
    // we only want this to be used if we iterable over doubles:
    is_iterator_of_type<
      T,
      decltype(void(aux::adl_begin(*(Container*)nullptr)), aux::adl_end(*(Container*)nullptr)) // ensure being and end work as bonus
    >::value
  >::type
>: std::true_type
{};

template<class Ch, class Tr, class Container>
auto operator<<( std::basic_ostream<Ch,Tr>& stream, Container const& c ) ->
  typename std::enable_if<
    is_container_of_type<double, Container>::value,
    decltype(stream)
  >::type
{
  stream << "'double' container: [ ";
  for(auto&& e:c)
    stream << e << " ";
  return stream << "]";
}

int main() {
  std::cout << std::vector<double>{1,2,3} << "\n";
  std::cout << std::set<double>{3.14,2.7,-10} << "\n";
  double array[] = {2.5, 3.14, 5.0};
  std::cout << array << "\n";
}

有了这个,不仅doubles 的数组在double 上算作容器,在其命名空间中定义beginend 函数的任何东西都可以作为容器,该函数返回超过double 的迭代器,该函数采用容器作为一个论点也有效。这与 for(auto&amp;&amp; i:container) 查找的工作方式相匹配(完美?相当好?),“容器”的良好工作定义也是如此。

但是,请注意,随着我们添加更多这些装饰,具有我们正在使用的所有 C++11 功能的当前编译器将越来越少。我相信上面在 gcc 4.6 中编译,但不是 gcc 4.5.*。

...

这里是带有一些测试框架的原始短代码:(如果你的编译器抛出它很有用,你可以在下面看到它出错的地方)

#include <iostream>
#include <type_traits>
#include <vector>
#include <iostream>
#include <set>

template<typename T, typename Iterator, typename=void>
struct is_iterator_of_type: std::false_type {};

template<typename T, typename Iterator>
struct is_iterator_of_type<
  T,
  Iterator,
  typename std::enable_if<
    std::is_same<
      T,
      typename std::iterator_traits< Iterator >::value_type
    >::value
  >::type
>: std::true_type {};

void test1() {
  std::cout << is_iterator_of_type<int, std::vector<int>::iterator>::value << "\n";
}
template<typename T, typename Container>
auto foo(Container const&) -> typename std::enable_if< is_iterator_of_type<T, typename Container::iterator>::value >::type
{
  std::cout << "Container of int\n";
}
template<typename T>
void foo(...)
{
  std::cout << "No match\n";
}
void test2() {
  std::vector<int> test;
  foo<int>(test);
  foo<int>(test.begin());
  foo<int>(std::set<int>());
}
template<typename Container>
auto operator<<( std::ostream& stream, Container const& c ) ->
  typename std::enable_if< is_iterator_of_type<int, typename Container::iterator>::value, std::ostream& >::type
{
  return stream << "int container\n";
}
void test3() {
  std::vector<int> test;
  std::cout << test;
  std::set<int> bar;
  std::cout << bar;
}
template<typename Container>
auto operator<<( std::ostream& stream, Container const& c ) ->
  typename std::enable_if< is_iterator_of_type<double, typename Container::iterator>::value, std::ostream& >::type
{
  return stream << "double container\n";
}
void test4() {
  std::vector<int> test;
  std::cout << test;
  std::set<int> bar;
  std::cout << bar;
  std::vector<double> dtest;
  std::cout << dtest;
}
void test5() {
  std::vector<bool> test;
  // does not compile (naturally):
  // std::cout << test;
}
template<typename Container>
auto operator<<( std::ostream& stream, Container const& c ) ->
  typename std::enable_if< is_iterator_of_type<bool, typename Container::iterator>::value, std::ostream& >::type
{
  return stream << "bool container\n";
}
void test6() {
  std::vector<bool> test;
  // now compiles:
  std::cout << test;
}
int main() {
  test1();
  test2();
  test3();
  test4();
  test5();
  test6();
}

以上大约一半是测试样板。 is_iterator_of_type 模板和 operator&lt;&lt; 重载是您想要的。

我假设T 类型的容器是任何类型定义为iterator 的类,其value_typeT。这将涵盖每个 std 容器和大多数自定义容器。

执行运行的链接:http://ideone.com/lMUF4i -- 请注意,某些编译器不支持完整的 C++11 SFINAE,可能需要愚蠢的操作才能使其工作。

留下的测试用例可帮助某人检查他们的编译器对这些技术的支持程度。

【讨论】:

  • 流媒体代码已经写好了。我正在寻找一种为所有容器类型声明它的方法。此解决方案不起作用,但如果您确实有“正确”的方法,我很想看到它。
  • 只需使用sizeof(c.begin()==c.end()),它更短,并额外检查返回类型是否具有可比性。或者在 C++11 中 sizeof(std::begin(c)==std::end(c))
  • @DrewDormann,上面的代码在 ideone.com 上的 C++11 编译器上编译和运行。并生成 [123]。如果你传递一个没有beginend 方法的类,operator&lt;&lt; 将不匹配。也许您的问题是您的编译器还没有enable_if?写起来也不难。 @MSalters——好主意,这确实使它更接近正确。
  • @DrewDormann 或者你没有发现上面的魔法。上面的代码使用 SFINAE 使 operator&lt;&lt; 仅当且仅当传入的类型具有 .begin().end() 方法时才匹配。如果缺少这样的方法,上面的operator&lt;&lt; 将无法匹配。我想知道您所说的“此解决方案不起作用”是什么意思——不起作用如何
  • 哦,你想要is_same,就像-&gt; typename std::enable_if&lt;std::is_same&lt; typename Container::value_type, X &gt;::value &amp;&amp; sizeof(c.begin() == c.end()), std::ostream &amp;&gt;::type 替换那个enable_if 子句一样。不完整的部分是全面的工业“这是一个容器”检测优于sizeof(c.begin() == c.end()),但实际上sizeof(c.begin() == c.end()) 非常接近“这是一个容器”,我很懒散。检测“鉴于它是一个容器,它是否是X 的容器”是微不足道的。
【解决方案2】:
template<template<class T, class A> class container>
std::ostream& opertaor << ( std::ostream&, const container<X, std::allocator<X> > &)
{
}

如果您的实现向量、列表等具有超过 2 个模板参数,这将不起作用。

【讨论】:

  • 这适用于vectordequelist,但不适用于其他一切。
  • 假设任何具有两个模板参数X, std::allocator&lt;X&gt; 的类型都是X 的容器?
【解决方案3】:

简单但不优雅 - 下一个维护您的代码的人可能会喜欢缺少花哨的模板!在实践中,我会将“打印”方法隐藏在 cpp 中,或者至少隐藏在 Detail 命名空间中。

#include <iostream>
#include <vector>
#include <deque>
#include <list>
#include <set>
#include <multiset>

class X {};

template <typename T>
std::ostream& Print(std::ostream& os, const T& container)
{
    for(auto ii = container.cbegin(); ii != container.cend(); ++ii);
        //etc
        //
    return os;
}

std::ostream& operator<<(std::ostream& os, const std::vector<X>& v) { return Print(os, v); }
std::ostream& operator<<(std::ostream& os, const std::deque<X>& v) { return Print(os, v); }
std::ostream& operator<<(std::ostream& os, const std::list<X>& v) { return Print(os, v); }
std::ostream& operator<<(std::ostream& os, const std::set<X>& v) { return Print(os, v); }
std::ostream& operator<<(std::ostream& os, const std::multiset<X>& v) { return Print(os, v); }

int main()
{
            // Example
    std::vector<X> v;
    std::cout << v;
}

【讨论】:

  • 既简单又优雅。有没有办法将它应用于不在 C++03 库中的容器?甚至是我还不知道的?
  • 只需添加更多重载。例如std::ostream&amp; operator&lt;&lt;(std::ostream&amp; os, MyContainerType&lt;X&gt;&amp; v);。您会知道何时需要添加重载,因为您会遇到编译器错误。另一个好处是重载可以通过容器定位 - 例如,假设您部署代码,并且 Print 函数和您的重载被“锁定”。如果有人创建/使用一个新容器,他们只需使用容器定义 operator&lt;&lt; 重载 - 它可以使用 Print 函数 - 所以他们不需要编辑您的代码来使用新的容器类型。
  • 我认为这些是相当可靠的解决方案,尽管他们没有回答这个问题。我同意,如果我放弃让它适用于X 的任意容器,或者如果我放弃实现“operator &lt;&lt;”,这将是一个很好的答案。
【解决方案4】:

如果您将问题稍微重新定义为为任何提供基于范围的 Widget 访问的类提供特殊的流式传输行为,而不是为所有 Widget 容器提供特殊行为,一种解决方案是:

  template <class Container>
  std::ostream& operator << (std::ostream &out, const Container &container) 
  {
    for(const Widget& c : container) {
      out << c;
      out.put(' ');
    }
    return out;
  }

这适用于 std::vectorstd::liststd::dequestd::set。如果您尝试流式传输不提供对 Widget 范围访问的内容,例如 std::list&lt;int&gt;,您将收到编译错误,因为 const Widget 引用无法绑定到 std::list&lt;int&gt; 中的整数。如果您为 std::list&lt;int&gt; 的运算符

【讨论】:

  • 将签名中的Container 替换为const Container&amp;,这看起来是显而易见的解决方案......
  • 这听起来像是对问题的合理重新定义。我认为它会导致比你提到的更多的错误,因为在将任何内容插入ostream 时会考虑该函数。例如,您不能在一个项目中拥有其中两个。
  • const & 是个好主意,我编辑了代码以匹配。 Drew 说得对,一个项目中不能有超过两个。
【解决方案5】:

虽然@razeh 有一个不错的解决方案,但如果您需要对X 容器而不是Y 容器进行特殊打印,您可以执行以下操作:

    // Types for which you want specialized streaming of containers
    // We need some identifiable typedef in these types
    struct  X { typedef void X_type; };
    struct  Y { typedef void Y_type; };


    // Wrappers for implementing streaming logic for each type        

template <typename C>
struct WrapX
{
    WrapX(const C& c) : c(c) { }
    const C& c;

    std::ostream& stream(std::ostream& os)
    {
         // Special container of X printing
         return os;
    }
};

template <typename C>
struct WrapY
{
    WrapY(const C& c) : c(c) { }
    const C& c;

    std::ostream& stream(std::ostream& os)
    {
        // Special container of Y printing
        return os;
    }
};

    // Wrap functions, by using a 'dummy' parameter
    // we can get the compiler to select the function based
    // on the incoming type

template <typename C >
WrapX<C> Wrap(const C& c,  typename C::value_type::X_type* = 0) { return WrapX<C>(c); }

template <typename C>
WrapY<C> Wrap(const C& c, typename C::value_type::Y_type* = 0) { return WrapY<C>(c); }



    // Overload - same problem as @razeh solution, this is a VERY generic
    // function and may clash with other declarations. Keep it closely confined to
    // where you need it.
template <typename C>
std::ostream& operator<<(std::ostream& os, const C& c) { return Wrap(c).stream(os);  }




int main()
{
    std::vector<X> vx;
    std::cout << vx;

        std::vector<Y> vy;
        std::cout << vy;
}

【讨论】:

    猜你喜欢
    • 2018-07-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-14
    • 1970-01-01
    • 2012-06-21
    相关资源
    最近更新 更多