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