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