DIY一個C++ traits來判斷enum是否有用戶自定義的operator

前段時間發現自己的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;
};

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章