DIY一个C++ traits来判断enum是否有用户自定义的operator<<

本文探讨了在C++中,由于enum与String类使用'+='连接导致的性能问题,以及如何利用TR1的is_enum traits进行优化。然而,当enum自定义了operator<<时,引入了新的挑战。作者研究了boost的has_left_shift traits,并提出了两种解决思路:通过取operator<<地址+sfinae限制参数类型,以及尝试屏蔽std命名空间。最终,作者发现ostream::operator<<(int)是成员函数,从而实现了has_global_left_shift的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前段时间发现自己的String库中有个bug:

String& operator+=(int);

String& operator+=(unsigned);

// 所有的整型、浮点型都有一个operator+=的重载

template <typename T>
String& operator+=(const T& t)
{
    std::stringstream ss;
    ss << t;
    this->operator+=(ss.str());
    return *this;
}
对所有的整型、浮点型、std::string,String::operator+=都有一个高效的实现,但唯独忽略了对enum的特殊处理,即使enum能通过integral promotion自动转换为int。

这样,在用“+=”连接字符串和enum类型的时候,String类会调用template版本的operator+=,具体操作包括一个局部std::stringstream变量的创建和析构,一次(低效的)stringstream::operator<<的调用,一个临时std::string变量的创建和析构(由ss.str()引起)。

这样的操作序列,性能之低可以想象。我的代码中对enum和String类有大量使用,导致程序的性能下降了30%。

问题的解决非常容易,TR1中(以及C++ 11中)有个叫做is_enum的traits,根据这个traits,很容易就能把来自enum的String::operator+=的调用定向到enum对应的int类型的String::operator+=的调用。

但是这样就引入了一个新问题,如果某个enum EnumA自己定义了operator<<:

std::ostream& operator<<(std::ostream&, EnumA);
那么按照老代码,它能够经由template版本的String::operator+=调用到这个operator<<,而打了patch后的String,在调用+=的时候却会出现不一致的行为:

enum EnumA
{
    E1
};

std:: ostream& operator<<(std::ostream& os, EnumA a)
{
    return os << "E1";
}

String str;
str += E1;
std::cout << E1 << std::endl;  // ok
std::cout << str << std::endl;  // oops!
当然,用户自定义enum的operator<<的情况毕竟不多,而性能对我来说更加重要,所以当时就忽略了“enum自定义operator<<”这种可能,换来性能的大幅提升。

但作为一个有追求的程序员,如果知道这种事情却不解决,显然有损coder的名号。

boost提供了一个有用的traits:has_left_shift,可以用来查询两个指定类型之间是否可以运行operator<<:

template <class Lhs, class Rhs=Lhs, class Ret=dont_care>
struct has_left_shift : public true_type-or-false_type {};
可惜它只能判断"lhs of typeLhs andrhs of type Rhs can be used in expressionlhs<<rhs",也就是说has_left_shift<ostream, AnyEnum>::value的值永远是true,无论AnyEnum是否有自定义的operator<<。上文已经提到,这是因为enum有向int的promotion,如果没有自定义operator<<,C++会帮她找到另外一个operator<<,即ostream自带的operator<<(int),对于只测试表达式"lhs<<rhs"能否成立的has_left_shift来说,这已经足够让它返回true了。

针对这个问题,产生了一些思路:

1.取operator<<地址+sfinae,可以实现限制operator<<的第二个参数必须是指定类型,这可以规避重载解析到ostream::operator<<(int)的情况,但这样无法处理operator<<在某个命名空间中的情况,&operator<<不会像std::cout << std::endl;一样根据NDL定位(尼玛,C++在这种地方都搞不一致)。

2.既然说到NDL,如果能在编译时通过某种手段屏蔽掉std命名空间,或者能通过某种traits自动获得某个类型所在命名空间的名称,也可以解决。唉,可惜C++里还没有这种语法。

此外还有一些别的思路,可惜统统行不通。此时已经写好取址+sfinae的实现:

template <typename T, std::ostream& (*F)(std::ostream&, const T&)>
struct Helper {
    Helper(int) {}
};

template <typename T>
char* Print(T t, Helper<T, &std::operator<<> p=0);

char Print(...);

template <typename T> 
struct HasLeftShift {
  const static bool value = sizeof(Print(*(T*)0)) == sizeof(char*);
};
注意:上面写成&std::operator<<才能取得std中operator<<的地址,这就是函数取址不支持NDL的结果。

测试中发现一个奇怪的现象:HasLeftShift<int>::value是false,HasLeftShift<std::string>是true,当时脑子有点轴,百思不得其解。

在尝试别的思路失败后,忽然想起,ostream::operator<<(int)莫非正如我写的一样,是个成员函数,所以取不到地址?

打开ISO C++标准一看,果然如此(27.6.2.1):

namespace std {
template <class charT, class traits = char_traits<charT> >
class basic_ostream : virtual public basic_ios<charT,traits> {
public:
    // ...
    basic_ostream<charT,traits>& operator<<(bool n);
    basic_ostream<charT,traits>& operator<<(short n);
    basic_ostream<charT,traits>& operator<<(unsigned short n);
    basic_ostream<charT,traits>& operator<<(int n);
    basic_ostream<charT,traits>& operator<<(unsigned int n);
    basic_ostream<charT,traits>& operator<<(long n);
    basic_ostream<charT,traits>& operator<<(unsigned long n);
    basic_ostream<charT,traits>& operator<<(float f);
    basic_ostream<charT,traits>& operator<<(double f);
    basic_ostream<charT,traits>& operator<<(long double f);
    // ...
};
接下来就容易多了,根据“int的operator<<已经定义为ostream的成员函数,而enum的operator<<只能定义为全局函数”这一点,很容易跟着boost::has_left_shift的思路写出一个"has_global_left_shift",从而判断enum是否有自定义的operator<<,然后就能完美地解决上文说到的,"性能提升的patch引起行为不一致"的问题,兼得鱼和熊掌:

template <typename T>
String& operator+=(const T& t)
{
    if (is_enum<T>::value)
    {
        if (!has_global_left_shift<T>::value)
        {
            operator+=(int(t));  // 无自定义operator<<的枚举,按int处理
        }
    }
    else
    {
        std::stringstream ss;
        ss << t;
        operator+=(ss.str());
    }
    return *this;
}
其中的if语句的测试条件在编译时就已经确定,完全可以优化掉。

has_global_left_shift的实现如下,简单的sfinae,命名和以上代码稍有差异:

template <unsigned int S>
struct Helper {
    typedef char Type[S];
};

template <typename L, typename R>
char TestGlobal(
    L lhs, R rhs, typename Helper<sizeof(operator<<(lhs, rhs))>::Type);

char* TestGlobal(...);

template <typename L, typename R>
char Test(L lhs, R rhs, typename Helper<sizeof(lhs << rhs)>::Type p);

char* Test(...);

template <typename L, typename R=L, bool global=false> 
struct HasLeftShift {
  const static bool value = sizeof(Test(*(L*)0, *(R*)0, 0)) == 1;
};

template <typename L, typename R> 
struct HasLeftShift<L, R, true> {
  const static bool value = sizeof(TestGlobal(*(L*)0, *(R*)0, 0)) == 1;
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值