测试左移运算符是否存在

Testing for the presence of the left shift operator

本文关键字:存在 是否 运算符 左移 测试      更新时间:2023-10-16

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

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

为了规避明显的响应,我尝试将模板化operator<<函数替换为用于各种类型的特定版本,但无济于事。 对于这两种类型,我还尝试了常量和 l-value/r 值说明符的各种组合(各种调整导致我看到一个编译器消息,指向带有 r 值 ostream 的operator<<重载)。 我也尝试过实现我自己的特质,这充其量只能给我与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);
}

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

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

这种机制在Stephan T. Lavavej的演讲和Herb Sutter的这篇旧文章中得到了很好的解释。

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

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

更新:

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

唉,扩展std命名空间(如果我们要添加新的operator <<重载,这就是我们要做的)是C++标准所禁止的。相反,允许的是专用于std命名空间中定义的模板函数(除非另有说明);但是,这在这里对我们没有帮助,因为没有模板的参数可以通过 vector<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;
            }
        } 
    } 
}

事情将开始发挥作用。

但有一个警告:虽然这在 GCC 4.7.2 中编译并运行良好,但具有预期的输出。但是,Clang 3.2 似乎要求在包含 has_left_shift.hpp 标头之前在boost::details::has_left_shift_impl中定义重载。我相信这是一个错误。

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

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

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

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

当它调用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因此有一种方法可以将标准或第三方类的自定义打印机打包到其自己的命名空间中:

#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';
}