潮.C++20 | explicit 關鍵字大解析

TJSW
8 min readFeb 22, 2020

--

學過 C++ 的各位同學最常在課堂上聽到的 explicit 這字我想就是掛在類別的 constructor 前面吧。但其實 explicit 還不只可以用在這,尤其是 C++20 之後也為這東西多賦予了新的技能 XD。像是 explicit (bool) 這種看上去不知道什麼意思的功能…

加在 constructor 前面

最基本最常見的應該是在說一個類別為了避免手殘在沒有意想到的地方發生隱式轉換 (implicit conversion),所以要求在 constructor 面前掛上 explicit 關鍵字,表示這個構建方式需要明確宣告。

explicit 最基本的用法:避免隱式轉換

可以看到,沒有標上 explicit 的 constructor 吃了一個 int,所以 main() 裡頭的 c1 可以順利地用 copy-initialization 的方式宣告。但吃字串常量的 constructor 被標記成 explicit,所以沒辦法直接以 C cabc = “abc”; 的方式宣告。

大家是不是覺得 C++ 就像這樣很簡單呢?我也曾經這樣以為 ^^

物件轉型

看這小標同學們可能一頭霧水,但對於 C++11 的聰明指標有涉略的同學們應該都用過 std::shared_ptr 或是 std::unique_ptr。其中有一項指標的用法我們一直都視為理所當然,但事實上沒那麼當然:

為什麼可以這樣用!?

以各位同學的智商一定知道上面程式該印出 “It is empty…”。邏輯上就是把 csp 當作指標判斷是否為空;但為什麼那段 if (csp) 能夠作動呢?std::shared_ptr 不過也是一個 C++ 類別啊,總不是因為他叫 ptr 就可以當指標拿來丟進 if 吧?

bool 轉型 operator

這是因為 std::shared_ptr 這些類別都實作了 C++ 的 conversion operator,這東西和 operator overloading 意思差不多。更詳細的用法和教學可以參照我寫過的自訂型別轉換。總之就是你只要實作 bool 轉型 operator,你的物件實例就可以在適當的時機變成 bool。也就是 C++ 所謂的 contextually convertible to bool (情境上可轉換為布林?)。簡單說就是可以直接丟進去在一堆布林運算元中間當作 truefalse 算來算去辣。來看個實作的例子:

bool conversion operator 的一個小例子

上面這個例子就是說,我做了一個叫 IsEven 的類別,用來判斷吃到的數字是奇數偶數。這類別加了一個 bool 轉型 operator,所以這些 IsEven 的實例們就可以丟到 if 裡面各種判斷運算啦,上面代碼輸出就是

0 and 2 are even numbers!
3 is not an even number!

0 也是偶數唷,請大家記得 ^^。

繞了這麼多,explicit 的第二個用途並不是在說 contextually converted to bool 的情況 (那你特別提到是在哈囉?),而是在說類別實作了任意型別的轉型 operator 之後能不能被隱式轉型成該型別的變數。直接上代碼

像上面那樣子,我對 C 這類別做了兩個型別轉換的 operator,一個是轉成 double (沒掛 explicit) 一個是轉成字串常量 (const char*,有掛 explicit)。

就和最前面的 explicit 用法一樣,有掛上 explicit 的那個函式沒辦法用在 main()s2 的 copy-initialization 上面;而 i2 就可以自然順暢地轉型成 double d2i2 想要變成 const char* 的話,就必須明確標注 static_cast 才可以變身。

再次特別用力強調:你在你的 bool 轉型 operator 前面有沒有掛 explicit 都跟能不能放在 if 裡面跟一堆 && || ! 一起計算沒有關係。但你要用 == 拿去比 true false 就有關係。

條件 explicit 函式 explicit (bool)

上面講的都是舊時代的過時產物,現在要說的是當代產物:C++20 發明的 explicit (bool):白話一句,你的 constructor 或是型別轉換 operator 可以依照編譯期運算出來的條件來決定要不要是 explicit

講起來很簡單,那為什麼需要這樣勒?假設我們今天有個類別他有些 constructor 是 explicit 有些不是,像這樣

這個類別 C 如果吃了字元或整數,就直接存進成員變數 n_,如果吃的是字串常量,代表的是字串的長度。為了使用方便,吃字元或整數的 constructor 允許隱式轉換,而不希望字串常量不小心隱式轉換成 C,所以掛上 explicit
這樣你就不能寫 C c = “abcd”;,而必須寫 C c{“abcd”};

然後,為了一些比較 generic 的需求,我們另外寫了一個 Wrapper 類別,他為了包裝各式各樣的類別,所以寫成一個樣板類別,想包裝的型別叫 T。因為我們不確定 T 是什麼,所以就用個萬用參數 U 來做 constructor 的參數型別。然後 Wrapper 裡面該有個成員 t 被 Wrapper 的 constructor 轉換一層來初始化。

我們就發現這個 W 類別樣版成功地包裝了三種不同方式初始化的 C 變成他們的成員變數 t。而且都是透過隱式轉換的方式。這顯然違背我們設計類別 C 的初衷:希望吃字串常量的初始化需要顯式轉換。怎麼會這樣呢?因為 W 的 constructor 沒有掛上 explicit,所以就都接受了隱式轉換 QQ。

如果說,我們想要讓 W 依照想包裝的型別 T 以及初始化 Wrapper W 的參數型別 U 來有條件地決定什麼時候要保有 explicit,而什麼時候不要?

當然是可以的,不然寫到這邊就直接 END 左轉掰掰。依照我們上面對 Wrapper 的定義,成員 t 是被 Wrapper 吃到的 u 初始化,因此 W 的 ctor 是 explicit 的時機應該是 U 不能被隱式轉換成 T 的時候,反之亦然

這個時候就是各種 type traits 花式出場的時候了,不解釋直接上解法代碼,懂 template meta programming 的人自然就懂。

透過 SFINAE 的匹配樣版機制還有 C++11/17 的 type traits 型別特徵性質,就能夠做到針對不同型別的 TU,各自找該是他們的歸宿,U 能轉成 T 就去找第一個 ctor 報到,不能的話就是第二個 ctor (有掛 explicit)。

但我們這就發現有一大堆同樣的重複冗餘的代碼啊,包含 type traits 連著 ctor 內容全都重複了,這樣不就是寫程式的壞味道了嗎?

因此 C++ 20 的 explicit (bool) 就來解救蒼生了。格式很簡單,長這樣

只要在 constructor 前面像以前一樣寫個 explicit,再寫個括號,裡面放個編譯時期能得出的 truefalse 就行。所以和上面的 SFINAE 解法融合一下,就變成這樣

explicit (bool) 取代 SFINAE 的解法

對比上面 SFINAE 的解法,我們省去了大半功能一樣卻重複的代碼,也省掉了為了匹配重載函數預設參數的 std::enable_if_t<>* = nullptr,瞬間就變得豁然開朗啦。而且由於不需要像 SFINAE 一樣對於不同的型別去具現化不同的函數類別樣版,所以這個解法的編譯時間也快了不少。

同學們有可能覺得說這個 Wrapper 的例子有點不實際,自己開發不太常用到,不過事實上大家從小時候最愛的夥伴就是這個例子的最佳受益者:std::pair

就講到這兒啦~除非自己需要設計這些比較泛型的程式,不然其實同學也不會有需要寫這些編譯時期的計算和檢查,同學看看就好 XD。

--

--