5

我正在尝试找到一个工作类型特征来检测给定类型是否具有用于 a 的左移运算符重载std::ostream(例如可与std::coutor互操作boost::lexical_cast)。boost::has_left_shift除了类型是 POD 或类型的 STL 容器的情况外,我已经取得了成功std::string。我怀疑这与 STL 类型或 operator<< 函数的特化有关。使用有效的左移运算符一般识别类型的正确方法是什么std::ostream?如果这不可行,是否有单独的方法来检测 POD 或 std::string 类型的 STL 容器上的左移运算符的过载?

下面的代码显示了我当前正在使用的代码,并演示了如何boost::has_left_shift无法检测到重载operator<<函数,即使它在下一行被调用。该程序在 GCC 4.5.1 或更高版本和 clang 3.1 中编译和工作。

为了规避明显的反应,我尝试将模板化operator<<函数替换为用于各种类型的特定版本,但无济于事。我还尝试了这两种类型的 const-ness 和 l-value/r-value 说明符的各种组合(各种调整使我看到编译器消息指向operator<<带有 r-value ostream 的重载)。我也尝试过实现我自己的 trait,它充其量给我的结果与boost::has_left_shift.

提前感谢您提供的任何帮助。如果能详细解释为什么会发生这种行为以及解决方案的工作原理,我也将不胜感激。我正在扩展我的模板知识的极限,并且很想知道为什么这不能像我想象的那样工作。

#include <string>
#include <vector>
#include <iostream>
#include <boost/lexical_cast.hpp>
#include <boost/type_traits/has_left_shift.hpp>

using namespace std;

struct Point {
    int x;
    int y;
    Point(int x, int y) : x(x), y(y) {}
    string getStr() const { return "("+boost::lexical_cast<string>(x)+","+boost::lexical_cast<string>(y)+")"; }
};

ostream& operator<<(ostream& stream, const Point& p)
{
    stream << p.getStr();
    return stream;
}

template <typename T>
ostream& operator<<(ostream& stream, const std::vector<T>& v)
{
    stream << "[";
    for(auto it = v.begin(); it != v.end(); ++it)
    {
        if(it != v.begin())
            stream << ", ";
        stream << *it;
    }
    stream << "]";
    return stream;
}

template <typename T>
void print(const string& name, T& t)
{
    cout << name << " has left shift = " << boost::has_left_shift<ostream , T>::value << endl;
    cout << "t = " << t << endl << endl;
}

int main()
{
    cout << boolalpha;

    int i = 1;
    print("int", i);

    string s = "asdf";
    print("std::string", s);

    Point p(2,3);
    print("Point", p);

    vector<int> vi({1, 2, 3});
    print("std::vector<int>", vi);

    vector<string> vs({"x", "y", "z"});
    print("std::vector<std::string>", vs);

    vector<Point> vp({Point(1,2), Point(3,4), Point(5,6)});
    print("std::vector<Point>", vp);
}
4

3 回答 3

7

它不起作用的原因是 C++ 有时有令人惊讶的(但动机良好的)解决函数调用的规则。特别是,名称查找首先在调用发生的名称空间和参数的名称空间(对于 UDT)中执行:如果找到具有匹配名称(或匹配的内置运算符)的函数,则选择它(或者如果找到多个,则执行重载决议)。

只有在 arguments 的命名空间中没有找到具有匹配名称的函数时,才会检查父命名空间。如果找到具有匹配名称的函数但不能用于解析调用,或者如果调用不明确,编译器将不会继续在父命名空间中查找以希望找到更好或更明确的匹配:相反,它会得出没有办法解决呼叫的结论。

Stephan T. Lavavej 的演讲Herb Sutter 的这篇旧文章很好地解释了这种机制。

在您的情况下,检查此运算符是否存在的函数位于boost命名空间中。您的参数要么来自std命名空间 ( ostream, string, vector) 要么是 POD ( int)。在std命名空间中,存在一个不可行的重载operator <<,因此编译器不会费心在定义重载的父(全局)命名空间中查找。它将简单地得出结论,在boost命名空间中完成的(模拟)调用以检查是否operator <<已定义无法解析。

现在boost::has_left_shift可能有一些 SFINAE 机制来将编译错误转换为失败的替换,并将分配falsevalue静态变量。

更新:

答案的原始部分解释了为什么这不起作用。现在让我们看看是否有办法解决它。由于使用了 ADL 并且std命名空间包含 的不可行重载operator <<事实上阻止了解析调用的尝试,因此可能会试图将可行的重载operator <<从全局命名空间移动到std命名空间中。

唉,C++ 标准禁止std扩展命名空间(如果我们要添加新的重载,我们会这样做operator <<) 。相反,允许的是专门化命名空间中定义的模板函数(除非另有说明);但是,这在这里对我们没有帮助,因为没有模板可以通过. 此外,函数模板不能部分特化,这会使事情变得更加笨拙。stdvector<int>

不过还有最后一种可能性:将重载添加到调用解析发生的命名空间。这是在 Boost.TypeTraits 机制中的某个地方。特别是,我们感兴趣的是进行调用的命名空间的名称。

在当前版本的库中,恰好是boost::detail::has_left_shift_impl,但我不确定它在不同 Boost 版本之间的可移植性。

但是,如果您真的需要解决方法,您可以在该命名空间中声明您的运算符:

namespace boost 
{ 
    namespace detail 
    { 
        namespace has_left_shift_impl
        {
            ostream& operator<<(ostream& stream, const Point& p)
            {
                stream << p.getStr();
                return stream;
            }

            template <typename T>
            std::ostream& operator<<(std::ostream& stream, const std::vector<T>& v)
            {
                stream << "[";
                for(auto it = v.begin(); it != v.end(); ++it)
                {
                    if(it != v.begin())
                        stream << ", ";
                    stream << *it;
                }
                stream << "]";
                return stream;
            }
        } 
    } 
}

事情将开始奏效。

There is one caveat though: while this compiles and runs fine with GCC 4.7.2 with the expected output. However, Clang 3.2 seems to require the overloads to be defined in the boost::details::has_left_shift_impl before the has_left_shift.hpp header is included. I believe this is a bug.

于 2013-01-28T20:31:28.217 回答
3

您的问题与命名空间和名称查找有关。当你使用

boost::has_left_shift<ostream , T>::value

代码位于命名空间 boost 中。它将在那里和模板参数的命名空间中搜索,但找不到匹配的operator<<. 编译器不会在全局命名空间中查找,因为所涉及的类型都不存在于那里。

另一方面,print函数本身位于全局命名空间中,并且会看到在其上方声明的运算符。

于 2013-01-28T20:30:03.150 回答
1

当它调用boost::has_left_shift<>()该函数时,会考虑使用参数相关名称查找找到的运算符重载。您的重载是全局的,但它们的参数来自std命名空间,这就是为什么在参数依赖名称查找中boost::has_left_shift<>()找不到它们的原因。

要解决您将重载移动到std命名空间的问题:

namespace std {

ostream& operator<<(ostream& stream, const Point& p); // definition omitted.

template <typename T>
ostream& operator<<(ostream& stream, const std::vector<T>& v); // definition omitted.

}

由于标准禁止在std命名空间中定义新的重载,因此有一种方法可以将标准或第 3 方类的自定义打印机打包到自己的命名空间中:

#include <vector>
#include <iostream>

namespace not_std {

// Can't directly overload operator<< for standard containers as that may cause ODR violation if
// other translation units overload these as well. Overload operator<< for a wrapper instead.

template<class Sequence>
struct SequenceWrapper
{
    Sequence* c;
    char beg, end;
};

template<class Sequence>
inline SequenceWrapper<Sequence const> as_array(Sequence const& c) {
    return {&c, '[', ']'};
}

template<class Sequence>
std::ostream& operator<<(std::ostream& s, SequenceWrapper<Sequence const> p) {
    s << p.beg;
    bool first = true;
    for(auto const& value : *p.c) {
        if(first)
            first = false;
        else
            s << ',';
        s << value;
    }
    return s << p.end;
}

} // not_std

int main() {
    std::vector<int> v{1,2,3,4};
    std::cout << not_std::as_array(v) << '\n';
}
于 2013-01-28T20:29:48.630 回答