Why Concepts? 實例分析 Function overload by type 當我們需要一個函數toString,傳入一個參數,Pseudo Code大概是這樣
1 2 3 4 5 6 7 8 9 template <typename T>std ::string toString (const T& input) { if (input have toString member function) { return input.toString(); } else { return "unknown format" ; } }
該怎麼做
C++98 Way 看看就好, 這手法現在也不需要理解了, 太舊了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 template <typename T>class has_toString {private : typedef char Yes; typedef Yes No[2 ]; template <typename U, U> struct really_has ; template <typename C> static Yes& Test (really_has <std ::string (C::*)() const , &C::toString>*) ; template <typename > static No& Test (...) ; public : static bool const value = sizeof (Test<T>(0 )) == sizeof (Yes); }; template <bool B, class T = void > // Default template version .struct enable_if { }; template <class T > // A specialisation used if the expression is true . struct enable_if <true, T> { typedef T type; }; template <typename T>std ::string toString (const T& input, typename enable_if<has_toString<T>::value, int >::type t = 0 ) { return input.toString(); } template <typename T>std ::string toString (const T& input, typename enable_if<!has_toString<T>::value, int >::type t = 0 ) { return "unknown format" ; }
我想你不會懷念他的 幾個缺點 – 沒有type_traits
,所以enable_if之類的要自己寫 – 每增加一個Signature偵測,就要多個類似has_toString
的東西出現 – Function overload的signagure要修改 – 奇醜無比
C++11 Way 僅列出跟C++98不同的地方
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 template <typename T>class has_toString {private : typedef char Yes; typedef Yes No[2 ]; template <typename C> static auto Test (void *) -> decltype(std::string{ std::declval<C const>().toString() }, Yes{}); template <typename > static No& Test (...) ; public : static bool const value = sizeof (Test<T>(0 )) == sizeof (Yes); }; template <typename T, typename std ::enable_if<has_toString<T>::value, int >::type t = 0 >std ::string toString(const T& input){ return input.toString(); } template <typename T, typename std ::enable_if<!has_toString<T>::value, int >::type t = 0 >std ::string toString(const T& input){ return "unknown format" ; }
基本上差不了多少,不過現在不用修改toString的Signature,順眼多了 C++14之後的更新基本上不可用,MSVC不支援,就不列出了
Boost Hana Solution 1 2 3 4 5 6 7 8 9 10 auto has_toString = boost::hana::is_valid([](auto && obj) -> decltype (obj.toString()) { });template <typename T>std ::string toString (T const & obj) { return boost::hana::if_(has_toString(obj), [] (auto & x) { return x.toString(); }, [] (auto & x) { return "unknown format" ; } )(obj); }
這基本上就很接近我們的Pseudo code,不過還要額外的Hana dependency
C++17 C++14之後的更新基本上不可用,MSVC不支援,就不列出了 不過我們可以用if constexpr
把兩個 toString合併成一個
Think more 我們只是檢查一個條件toString
,如果有復合的條件約束的話該怎麼辦, 假設我們還要檢查一個ver
的member variable或者這個typeu定義了一個value_type,上面的方式就變得無力
C++ 20 Concept Solution 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 template <typename T>concept has_toString = requires (const T &t) { typename T::value_type; { t.ver } -> int ; { t.toString() }->std ::same_as<std ::string >; }; template <typename T>std ::string toString (const T& input) { if constexpr (has_toString<T>) return input.toString(); else return "unknown format" ; }
配合C++17的if constexpr,很漂亮的解決了上述的難題
Debug Template Code 在寫Generic template code的時候,往往要跟Error message打交道 有了Concept約束之後,比較容易觀測出到底哪裡錯了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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; return v; } int main () { int b = 10 ; auto y = get_value(b); }
錯誤訊息如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <source >:2:1: error: 'concept' does not name a type 2 | concept have_value = requires(T a) { | ^~~~~~~ <source >:2:1: note: 'concept' only available with '-fconcepts' <source >:7:1: error: 'requires' does not name a type 7 | requires have_value<T> | ^~~~~~~~ <source >: In function 'int main()' : <source >:18:12: error: 'get_value' was not declared in this scope 18 | auto y = get_value(b); | ^~~~~~~~~ Compiler returned: 1
很明顯的我們的Type不符合Concept的要求 這個範例簡單到就算用原先的方式,有經驗的Porgrammer一樣能夠找出錯誤 如果你的Code是一層一層的Template累加起來,就知道這東西多有用了
How to use Concept Four Ways Requires Clause 1 2 3 template <typename T>requires std ::integral<T>void log (T&& x) ;
Trailing Requires Clause 1 2 template <typename T>void log (T&& x) requires std ::integral<T>
雖然比起第一種方式顯得冗長,不過有種特異功能第一種方式做不到
1 2 template <typename T>void log (T&& x) requires std ::integral<decltype (x) >
由於Trailing Requires Clause可以看到整個Function signature, 因此可以使用decltype
做處理
Constrained Template Parameters 1 2 template <std ::integral T>void log (T&& x)
很顯然的, T這個類型必須滿足std::integral
的約束 這方案還有一種好處,在不支援C++20的Compilier,只要把std::integral
換成typename
就能照常編譯了,不過享受不到Concept的好處 不過可以在C++20檢查過之後Poring回Pre-C++20的Compiler
Abbreviated function template 顧名思義,這種方式只能在 Function template上用,是一種語法糖 看看以下這個案例
1 2 3 4 Arithmetic auto sub (Arithmetic auto fir, Arithmetic auto sec) { return fir - sec; }
基本上等價於Constrained Template Parameters
的
1 2 3 4 5 template <Arithmetic T, Arithmetic T2>auto sub (T fir, T2 sec) { return fir - sec; }
比起上面的寫法,連template宣告都省下來了,不過上面阿寫法可以蛻化為Pre-C++20,這不行
Function overload Concept允許function overload,比起一般的template function, 通過concept檢驗的function會優先採用 例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void overload (auto t) { std ::cout << "auto : " << t << std ::endl ; } template <typename T>requires std ::is_integral_v<T>void overload (T t) { std ::cout << "Integral : " << t << std ::endl ; } void overload (long t) { std ::cout << "long : " << t << std ::endl ; } int main () { std ::cout << std ::endl ; overload(3.14 ); overload(2010 ); overload(2020l ); std ::cout << std ::endl ; }
Customize Concept The simplest concept Concept就是Compile-time的boolean expression
1 2 template <typename T>concept superfluous = true ;
既然是個boolean expression,就能做所有boolean operator支援的操作
1 2 3 4 template <typename T>concept integral = std ::is_integral_v<T>;template <typename T>concept number = std ::integral<T> || std ::floating_point<T>;
More complext concept 定義一個能支援遞增的Concept
1 2 3 4 5 template <typename T>concept you_can_increment_it = requires (T x){ {++x}; };
定義能夠進行四則運算的Concept, 注意這邊需要兩個Type
1 2 3 4 5 6 7 8 template <typename X, typename Y>concept they_are_mathsy = requires (X x, Y y){ { x * y }; { x / y }; { x + y }; { x - y }; };
對Return type有要求的Concept
1 2 3 4 5 6 template <typename T>concept you_can_increment_it = requires (T x){ {++x} -> std ::same_as<T>; {x + x} -> std ::convertible_to<int >; };
檢查type是否有member structure和member function
1 2 3 4 5 6 7 8 9 10 11 12 template <typename T>concept its_a_dragon = requires (T x){ typename dragon_traits<T>; typename T::dragon_clan_ptr; {x.breathe_fire()}; {x.dragon_breath_firepower()} -> std ::convertible_to<uint>; };
Concept all the thing 當然,你也可以在其他地方加上Concept
1 Integral auto integ = getIntegral(10 );
你的return value自然就要符合Integral的約束,幫助我們在Code Maintenance和Refactoring中得到優勢
Conclusion 比起C++Module和C++Coroutine改變遊戲規則的重要性比起來,Conceptˋ只能算是錦上添花,不過對Library developer來說還是有用的
Reference Is it possible to write a template to check for a function’s existence? Notes on C++ SFINAE An introduction to C++’s SFINAE concept: compile-time introspection of a class member Substitution Failure is Error and Not An Error. C++20: Concepts, the Details C++20 Concepts C++20: Define Concepts