關(guān)于C++菱形運算符深度解析
最近在翻《c++函數(shù)式編程》的時候看到有一小節(jié)在說c++14新增了“菱形運算符”。我尋思c++里好像沒什么運算符叫這名字啊,而且c++14新增的功能很少,我也不記得有添加這種語法特性。一瞬間我有些懷疑我的記憶了,所以為了查漏補缺,我寫了這篇文章。
什么是菱形運算符
這個概念在Java里比較多見:
List<String> myList = new ArrayList<>();
這東西在Java里的學名是diamond operator,表示使用泛型類并且類型參數(shù)在左側(cè)的表達式已給出因此在右側(cè)可以省略。
簡單的說就是讓你少寫幾次重復的類型參數(shù)。因為看起來像個菱形所以得名菱形運算符。
然后我們偶爾會在c++里看到形狀上很相似的東西:
std::sort(vec.begin(), vec.end(), std::greater<>());
<>出現(xiàn)在模板的特化中是我們所熟悉的,但這個std::greater<>()是什么呢?
c++沒有菱形運算符
先說結(jié)論,從語言標準來說,c++里沒有什么菱形運算符。
c++20里雖然新增了一個運算符operator<=>,但這個和所謂的菱形運算符沒有任何關(guān)系。
那問題來了,std::greater<>()是什么以及為什么書里說是c++14新增的特性呢?難道書里瞎說的嗎?但事實是這樣的示例代碼在c++14以及之后的標準下可以正常編譯運行,而且這本書的質(zhì)量尚可,雖然會在措辭上犯些小錯(比如c++沒有菱形運算符)但不至于花大篇幅去胡說八道。
當然,要想回答這個問題我們得先復習點基礎(chǔ)知識。
<>在c++里的作用
先說結(jié)論,在c++里看到<>,絕大多數(shù)都是在為模板提供類型參數(shù),當然這種東西我們不討論:(a<1, 2>b),這里<和>是在兩個不同的表達式里。
那既然用來提供類型參數(shù),那為什么可以啥都不提供呢?答案是有兩類情況確實可以。
第一類是在函數(shù)模板上,類型參數(shù)可以自動推導時:
template <typename T>
void f(const T&)
{
std::cout << "f<T>\n";
}
template <>
void f(const int&)
{
std::cout << "f<int>\n";
}
void f(const int&)
{
std::cout << "f\n";
}
int main()
{
f(1); // f
f<>(1); // f<int>
f(1.2); // f<T>
}非模板函數(shù)在重載決議中的優(yōu)先級總是高于模板的,因此f(1)這樣的表達式總是會用到最下面定義的那個非模板函數(shù)f。這時候我們可以用f<int>(1)來直接調(diào)用函數(shù)模板f,而函數(shù)模板的類型參數(shù)如果能從參數(shù)推導出來的話,可以不明確給出(也就是后面的f(1.2)那樣的),而在我們現(xiàn)在這句表達式里,我們既要明確使用函數(shù)模板,又想讓類型參數(shù)被自動推導,就得使用f<>(1)。
另一種情況不分類模板還是函數(shù)模板,當模板的類型參數(shù)有默認值時,可以靠<>來使用這些默認值:
template <typename T = void>
struct Wrapper
{
using wrappered = T;
};
// Wrapper<> 等于 Wrapper<void>
static_assert(std::is_same_v<Wrapper<>::wrappered, Wrapper<void>::wrappered>);在第二種情況下,因為沒顯示給出類型參數(shù),且這里沒法使用類型推導,因此編譯器使用了類型參數(shù)的默認值,這里是void。
觀察比較仔細的話其實會發(fā)現(xiàn)上面兩種情況其實是一件事,<>相當于沒有顯示給出任何類型參數(shù),于是對這些沒有顯示指定的類型參數(shù),編譯器會先嘗試類型推導,如果沒法推導則會檢查這些類型參數(shù)是否有默認值,有就利用默認值。如果上面這兩步都沒法得到能正常使用的類型參數(shù),模板會被SFINAE淘汰或者報出編譯錯誤。
這并不是什么新語法,是從有模板開始就一直存在的規(guī)則。
現(xiàn)在我們可以看看std::greater<>()是什么了,首先std::greater是個類模板,然后它接受一個類型參數(shù),這個參數(shù)在c++14之后有了默認值void,因此std::greater<>()是std::greater<void>()。
c++14中究竟添加了什么
既然c++14并沒有添加“菱形運算符”,那究竟新增了什么呢?
在已經(jīng)知道了std::greater<>()的真身后,找起來就很容易了,所以我很快找到了對應的新特性:n3421
這個特性是這樣的:原先我們要用標準庫提供的謂詞模板,需要自己指明參數(shù)類型,這樣寫起來很麻煩而且對于那種嵌套的或者元素類型復雜的容器來說寫明參數(shù)類型不僅費時而且費力,更要命的是對于map,一不小心是會有性能問題的:
for_each(map.begin(), map.end(), std::pred<std::pair<std::string, int64_t>>());
上述代碼的問題在于正確的參數(shù)類型應該是std::pair<const std::string, int64_t>,我們漏掉了const,這會導致pair整個被復制一遍,性能是無比底下的。要徹底避免這種錯誤,就得利用自動類型推導。
然而前面說了,標準庫提供的謂詞基本全是類模板,類模板的模板參數(shù)要么依賴默認值要么得顯示指定,怎么才能依賴自動推導呢。
于是這個新特性最精彩的地方來了:原先的模板的調(diào)用運算符不是模板參數(shù)也是定死的,但我們可以新加一個默認參數(shù),然后針對這個默認參數(shù)的類型進行完全特化,在特化里提供一個泛型的operator(),這樣就能利用函數(shù)模板來自動推導參數(shù)類型了,而且以前的代碼不受影響。
默認參數(shù)的設(shè)置也是有講究的,需要用一個謂詞用不到的且不會影響老代碼的類型,運氣不錯,void正好符合條件(void上幾乎沒法做什么操作,因此也不會被指定給這些謂詞做類型參數(shù)),因此現(xiàn)在的greater的代碼是下面這樣的:
// 注意默認值是void
template <typename T = void> struct greater {
constexpr bool operator()(const T& lhs, const T& rhs) const
{
return lhs > rhs;
}
};
// 針對greater<void>的完全特化
template <> struct greater<void> {
template <class T, class U> auto operator()(T&& t, U&& u) const
-> decltype(std::forward<T>(t) > std::forward<U>(u))
{ return std::forward<T>(t) > std::forward<U>(u); }
};當使用std::greater<T>()的時候,代碼的邏輯和原來一樣,當使用std::greater<void>()的時候,返回的Functor的函數(shù)調(diào)用運算符是個模板,可以自己推導參數(shù)類型和返回值類型。至于為啥greater<void>的內(nèi)部構(gòu)造可以和其他情況實例化的greater區(qū)別這么大,這個是c++的特性:模板的不同實例之間是可以異構(gòu)的。
而且因為類型參數(shù)的默認值就是void,因此可以簡寫成std::greater<>()。
所以c++14只是給標準庫里可以代替運算符的模板們增加了默認類型參數(shù)和一個泛型的調(diào)用運算符,利用這些可以簡化代碼并確保類型安全。
真相是其實沒啥菱形運算符,只是利用了以前就存在的模板的特性簡化了標準庫的使用,讓人少寫點字。達成的效果倒是和Java的菱形運算符差不多。
總結(jié)
顯然書里有夸大成分,老話說盡信書不如無書,還得小心檢驗才是。
順便我們復習了現(xiàn)代c++的重要原則:能依賴自動類型推導的地方,沒必要自己手寫。
因此應該多寫這樣的代碼:std::sort(vec.begin(), vec.end(), std::greater<>());。
不過還有最后一個問題,為啥不直接用lambda呢?那是因為能指定類型參數(shù)的泛型lambda要在c++20才出現(xiàn),在這之前想要讓lambda完全做到類型安全得費點功夫,而且lambda整體上也不如直接用標準庫提供的std::greater<>()、std::less<>()之類的簡潔易懂。
到此這篇關(guān)于C++里的菱形運算符的文章就介紹到這了,更多相關(guān)C++菱形運算符內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
VC實現(xiàn)動態(tài)菜單的創(chuàng)建方法
這篇文章主要介紹了VC實現(xiàn)動態(tài)菜單的創(chuàng)建方法,需要的朋友可以參考下2014-07-07

