【问题标题】:Best way to specialise operator<< for std::ostream and std::vector with generic template functions?使用通用模板函数为 std::ostream 和 std::vector 专门化 operator<< 的最佳方法?
【发布时间】:2016-08-01 14:04:52
【问题描述】:

我在使用标准指定的两阶段查找时遇到问题,并且(正确地)由 clang 实现,与 std::ostreamstd::vectoroperator&lt;&lt; 过载有关。

考虑一个非常通用的模板函数,它将其参数转换为流(仅在递归时才真正有用,但简单的示例足以引发问题):

// generic.h
template<typename Stream, typename Arg>
void shift(Stream& s, Arg& arg) { s << arg; }

这个 generic.h 可以在整个项目中使用。然后在其他文件中,我们要输出一个std::vector,所以我们定义了一个重载

// vector.h
#include <iostream>
#include <vector>
std::ostream& operator<<(std::ostream& s, std::vector<int> const& v) {
  for(auto const& elem : v) { s << elem << ", "; }
  return s;
}

而主文件,我们首先(间接)使用generic.h,然后,由于其他一些包含,向量重载:

// main.cpp
#include "generic.h"
#include "vector.h"

int main() {
  std::vector<int> v{1,2,3,4,5};
  shift(std::cout, v);
}

此代码已被 GCC (5.4.0) 和 ICC (16.0) 接受,但 clang 抱怨 call to function 'operator&lt;&lt;' that is neither visible in the template definition nor found by argument-dependent lookup

令人讨厌的是,clang 是正确的,我想在我的代码中解决这个问题。据我所知,有三个选项:

  • operator&lt;&lt; 的定义移到shift() 之前。这样做的缺点是,当包含一些(可能是其他)间接包含generic.hvector.h 的文件时,还必须注意正确排序。

  • 使用自定义命名空间,将 std 所需的所有内容导入该命名空间,并在该命名空间内的新命名空间类上定义运算符,以便 ADL 可以找到它。

  • std 命名空间中定义 operator&lt;&lt;。我认为这是未定义的行为。

我错过了任何选择吗?一般而言,为std-only 类的函数定义重载的最佳方法是什么(如果我想转移NS::MyClass,则问题不存在,因为那时我可以在NS 中定义运算符)。

【问题讨论】:

    标签: c++ templates c++11 language-lawyer


    【解决方案1】:

    不要为您无法控制的类型重载运算符,例如:

    std::ostream& operator<<(std::ostream& s, std::vector<int> const& v);
    

    改为创建一个小型适配器类并为此定义运算符,例如:

    template<typename T> struct PrintableVector {
      std::vector<T> const* vec;
    }
    
    template<typename T>
    std::ostream& operator<<(std::ostream& s, PrintableVector<T> v) {
      for(auto const& elem : *v.vec) { s << elem << ", "; }
      return s;
    }
    

    可以这样使用:

    shift(std::cout, PrintableVector<int>{&v});
    

    您可以将适配器放在您喜欢的任何命名空间中,并将重载的运算符放在同一个命名空间中,以便 ADL 可以找到它。

    这避免了查找问题,不需要向命名空间std 添加任何内容,并且不会尝试唯一定义打印vector&lt;int&gt; 的含义(如果某些情况可能会导致程序的其他部分出现问题)其他代码假定向量不可打印,或尝试为它们定义自己的重载)。

    【讨论】:

    • 如果你要使用指针,你可能应该检查nullptr。否则,只需使用引用或可选类型
    • 是的。显然,可以进行一些调整,具体取决于您想要如何使用它或您想要如何防御。引用的缺点是类型不可分配。如果这无关紧要,请使用参考。答案的重点是“使用适配器而不是重载您不拥有的类型的运算符”,而不是“这里是如何编写一个万能的通用适配器”。
    • 引用不可分配的缺点:使用如此轻量级的包装器,您每次都可以简单地重新创建一个,所以我同意@KABoissonneault 使用引用而不是指针。
    • 我不确定这是否是问题的最终解决方案。我完全同意所描述的功能集(没有全局决定如何显示内容等)。但是一旦你有了Printable 基础设施,混合代码库时可能会出现问题。 operator&lt;&lt; 实现中的某些类型需要转换,而有些则不能...
    【解决方案2】:

    我关注Jonathan’s advice 并使用包装器Printable&lt;&gt; 来定义operator&lt;&lt;。通过使这个包装器也可以隐式转换为原始类型,我可以处理只有Printable&lt;T&gt; 可打印以及T 本身也是可打印的两种情况。代码如下:

    template<typename T>
    struct Printable {
      T const& ref;
      Printable(T const& ref) : ref(ref) { }
      operator T const& () { return ref; }
    };
    
    template<typename T>
    Printable<T> printable(T const& in) { return Printable<T>(in); }
    
    template<typename Stream, typename Arg>
    void shift(Stream& s, Arg& arg) {
      s << printable(arg);
    }
    
    #include <iostream>
    #include <vector>
    std::ostream& operator<<(std::ostream& out, Printable<std::vector<int> > const& v) {
      for(auto const& elem : v.ref) { s << elem << ", "; }
      return s;
    }
    
    struct MyClass { };
    std::ostream& operator<<(std::ostream& s, MyClass const& m) {
      return s << "MyClass\n";
    }
    
    int main() {
      std::vector<int> v{1,2,3};
      MyClass m;
    
      shift(std::cout, v);
      shift(std::cout, m);
    }
    

    这样做的好处是,在调用shift() 时,我不必关心我的变量的类型。只有在 operator&lt;&lt; 的定义中,我必须小心,以及在使用这样的运算符时。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-01-30
      • 2016-09-16
      • 2017-11-10
      • 2012-09-08
      • 2015-07-05
      • 1970-01-01
      • 2012-12-08
      相关资源
      最近更新 更多