Template 是 C++ 一個泛型的重要功能。透過 template 可以讓開發者只要寫一次,就可以針對不同的型別的資料,來做處理。
他雖然用起來很方便,但是大部分情況下都缺少對於需求型別的描述,所以如果沒用好寫錯了,就很有可能因為使用了不符合需求的型別,而導致無法正確編譯;而這個時候,編譯器給個錯誤訊息往往也會過於雜亂、讓開發者難以理解、也難以找到真正的問題點。
比如說下面這個簡單的範例程式:
struct A { int value; }; template<typename T> auto get_value(const T& a) { auto v = a.value; // do some thing return v; } int main() { A a{ 10 }; auto x = get_value(a); }
這個程式裡面,get_value() 這個函式會去取得傳入參數 a 的 value 這個成員變數的值,然後經過處理後、回傳結果的值。
上面的程式由於 A 這個結構有定義 value,所以使用上並沒有問題,可以正確編譯、也可以正確執行。
但是如果其他想要使用 get_value() 這個函式的人不知道使用的型別需要有 value 這個成員的話,那會怎樣呢?比如說,把傳入參數的型別從 A 變成 int 的話,main() 會變成:
int main() { int a{ 10 }; auto x = get_value(a); }
這時候,就會出現錯誤而無法完成編譯了~
而在 Visual Studio 中的編譯錯誤,訊息會如下:
1>1>concept.cpp 1>concept.cpp(9,1): error C2228: '.value' 的左邊必須有類別/結構/等位 1>concept.cpp(9,1): message : 類型為 'const T' 1>concept.cpp(9,1): message : with 1>concept.cpp(9,1): message : [ 1>concept.cpp(9,1): message : T=int 1>concept.cpp(9,1): message : ] 1>concept.cpp(17): message : 請參考正在進行編譯的函式 樣板 具現化 'auto get_value(const T &)' 1>concept.cpp(17): message : with 1>concept.cpp(17): message : [ 1>concept.cpp(17): message : T=int 1>concept.cpp(17): message : ] 1>concept.cpp(11,1): error C3536: 'v': 無法在初始化之前使用 1>專案 "Concept.vcxproj" 建置完成 -- 失敗。
而彙整的錯誤報告有兩個,分別是:
- 錯誤 C2228:'.value' 的左邊必須有類別/結構/等位 S:CppTestConceptconcept.cpp 9
- 錯誤 C3536:'v': 無法在初始化之前使用 S:CppTestConceptconcept.cpp 11
如果點選上面的錯誤、Visual Studio 所跳到的程式碼,都會在 get_value() 內,讓人較難以判斷;而如果程式的 template 用得更複雜的話,在不熟悉這類的錯誤、同時也難以認真去讀編譯器輸出的訊息(message)的狀況下,那要判斷錯誤原因的狀況,是相當困難的…
為了解決這樣的問題,C++20 引進了所謂的「concepts」,用來針對 template 的型別,做額外的描述。
這部分的介紹可以參考《Constraints and concepts》或《C++20: Concepts, the Details》。而 Visual Studio 2019 在 16.3 後,也已經有部分支援 concepts(參考《C++20 Concepts Are Here in Visual Studio 2019 version 16.3》)了~這篇的程式測試,基本上都是在 Visual Studio 上測試的。
不過老實說由於這東西應該還算是在很早期的階段,所以相關介紹感覺還不算完整,Heresy 自己邊看邊摸,也不知道到底是不是完全正確;如果有錯的話,也麻煩指正一下了。
總之,如果要使用 concept 的話,上面的程式碼大致上就會修改成類似下面的樣子:
template<typename T> concept have_value = requires(T a) { a.value; }; template<typename T> requires have_value<T> auto get_value(const T& a) { auto v = a.value; // do some thing return v; } int main() { int b = 10; auto y = get_value(b); }
其中,第一段就是在定義一個名為「have_value」的 concept。
template<typename T> concept have_value = requires(T a) { a.value; };
在 C++20 中,所謂的 concept 基本上就是一些「需求」(requirement)的集合,用來驗證型別是否符合需求用的。
而這個例子裡面,就是簡單地透過驗證 a.value; 這個式子是否合法、來確定型別 T 是否有 value 這個成員。
在定義好了 have_value 這個 concept 後,接下來則是幫本來的 get_value() 加上一行
requires have_value<T>
告訴編譯器以及使用者,這個函式的 template 型別 T 需要符合 have_value 這個 concept。
在這樣修改完成後,編譯的錯誤會變成是:
1>concept.cpp 1>concept.cpp(21,11): error C2672: 'get_value': 找不到相符的多載函式 1>concept.cpp(21,22): error C7602: 'get_value': 未滿足相關聯的限制式 1>concept.cpp(13): message : 請參閱 'get_value' 的宣告 1>專案 "Concept.vcxproj" 建置完成 -- 失敗。
基本上,錯誤算是更明確了!
而如果點選上面的錯誤的話,他也會跳到呼叫 main() 裡面呼叫 get_value() 的地方,而不會再直接進到 get_value() 內~
除了錯誤訊息更明確外,由於在 get_value() 宣告就明確地寫了它需要符合的條件,對於要使用它的開發者,也會更容易地去判斷自己要使用的型別,是否符合函式的設計了~
在 template 函式要使用,其實總共有三種寫法。第一種寫法(Requires Clause),就是像上面一樣,寫在 template<> 的後面的形式:
template<typename T> requires have_value<T> auto get_value(const T& a){ ... }
第二種方法(Trailing Requires Clause),則是把 requires 加在後面:
template<typename T> auto get_value(const T& a) requires have_value<T>{ ... }
另外,甚至可以直接寫成下面更為精簡的形式(Constrained Template Parameters):
template<have_value T> auto get_value(const T& a) { ... }
而除了針對 template 函式來做需求的限制外,他也可以用來做 template specialization。
比如說上面的程式就可以寫成:
struct A { int value; }; template<typename T> concept have_value = requires(T a) { a.value; }; template<have_value T> // have_vale auto get_value(const T& a) { auto v = a.value; // do some thing return v; } template<typename T> // generic auto get_value(const T& a) { auto v = a; // do some thing return v; } int main() { A a{ 10 }; auto x = get_value(a); // use have_vale int b = 10; auto y = get_value(b); // use generic }
透過這樣撰寫兩個不同的 get_value(),就可以讓編譯器去判斷呼叫的時候要去用哪的版本了。在這個狀況下,只要所傳入的參數型別符合 have_value 這個 concept 的話,他就會去使用第一個版本,否則就會去使用第二個一般性的版本。
所以透過 concept 來做 template specialization,其實也算是滿實用的~
而除了用在 template function 上,cooncept 也能用在 template class 上。例如:
template<typename T> requires have_value<T> class CValue {};
或是
template<have_value T> class CValue {};
此外,他也可以用在 template class 的 member funtion;例如:
template<typename T> class CValue { public: T m_val; int get_value() const { return m_val; } int get_value() const requires have_value<T> { return m_val.value; } };
以上面的例子來說,雖然在 CValue 這個類別裡面同時有宣告兩個 get_value() 的成員函式,但是由於條件(requires)不同,所以其實並不會同時存在,所以並不會出現重複定義的狀況。
而在使用時,編譯器也會根據型別是否符合 have_value 這個 concept,來產生不同的版本。
這篇大概就先記錄這些,下一篇再來整理一下怎麼定義一個 concept 吧~
主要參考: