轉眼間,C++20的標準已經發布快兩年了。不少C++的開源專案也已經將標準升級到最新的C++20了,筆者也開啟了新標準的學習歷程了。所以借這系列的博文,記錄下筆者學習新標準的一些心得與吐槽~~
作為C++20系列的第一篇開篇之文,就要從千呼萬喚始處理的concept
聊起了,後續很多新的feature的實現,也仰賴新的concept
的實現,後續筆者的文章也會逐步展開。OK,開始我們C++20旅程的第一站:concept
1.First Look
先從一個群友的一個實際的問題出發,我們來看看concept
可以解決什麼問題。是怎麼樣通過coding實現的。
- SFINAE
熟悉C++模板程式設計的小夥伴肯定第一時間想到通過SFINAE的方式來解決,讓筆者來解決這個問題的話,會寫出下面的程式碼:
template <typename T>
T test(T a) {
static_assert(!std::is_same_v<void, decltype(a + a)>);
static_assert(!std::is_same_v<void, decltype(a - a)>);
static_assert(!std::is_same_v<void, decltype(a * a)>);
static_assert(!std::is_same_v<void, decltype(a / a)>);
return a;
}
這裡寫的程式碼是一個略微Trick的表達,利用decltype
去獲取操作符計算後的型別,然後用std::is_same_v
進行一個其實沒什麼意義的型別比較,來滿足static_assert
的語義,最終滿足我們對模板型別T
的一些約束。
- concept
顯然上面的方式是很不直觀的,雖然能達到我們們的目的,但是從程式碼優雅角度來說是一種較差的選擇實現。
我們來看一下用C++20提供給我們的Concept是如何解決這個問題的:
template <typename T>
concept Cal = requires (T a) {
a + a;
a - a;
a * a;
a / a;
};
template <typename T>
requires Cal<T>
T test(T a) {
return a;
}
這是通過concept
來實現的一個型別約束方式,Cal
代表著一個concept的實現,requires
中花括號的內容就代表了對於型別T
的約束,要滿足下面的操作符
a + a;
a - a;
a * a;
a / a;
Bingo! 似乎C++20給了我們一個更好的trait,接著往下看,我們繼續來細探Concept的實現。
2. How to use
- concept的定義
這裡寫了一個例子,我們們基於這個例子來展開:
template <typename T>
concept Cal = requires (T a) {
a + a;
typename T::type;
{a + a} -> std::same_as<float>;
require Concept2<T>;
};
這裡定義了4個requires的要求,只有滿足這4個條件才能通過concept
的限制,正常進行編譯。
1). a + a
這個是最簡單的,該表示式能通過編譯則代表符合要求,這裡不會進行實際的計算。
2). typename T::type
代表需要,T
型別定義了type
型別,才符合要求
3). {a + a} -> std::same_as<float>
這裡的{}
代表了decltype(a + a)
之後的型別需要滿足後面的concept
的需求。這是一個語法糖,也可以通過這樣來實現:requires std::is_floating_point_v<decltype(a + a)>
4). 最後的require Concept2<T>
這代表了concept
的巢狀邏輯。requires
後面可以帶任意的concept
- concept的使用
瞭解了concept
定義之後,我們就可以利用concept
來進行模板型別的約束了。
這裡“回字有四種寫法”,大家可以選擇自己喜歡的方式來使用。(真搞不懂搞這麼多寫法幹什麼,不能統一一下嗎?, 下面介紹的順序隨筆者的偏好以此遞減)
template<typename T>
requires Cal<T>
T test(T a)
{
return a + a;
}
1). 這是筆者最認可的一種書寫方式,語義明確,在模板型別定義之後明確對它的要求。
template<Cal T>
T test(T a)
{
return a + a;
}
2). 也還行吧,模板引數前指定了concept
,也比較明確直觀。
Cal test(Cal a)
{
return a + a;
}
3). concept
命名一長就顯得有些瑣碎了,並且把concept
當型別使用了,看起來有些怪異了。
template<typename T>
T test(T a) requires Cal<T>
{
return a + a;
}
4). 把concept
寫後面的都是異端,當然如果你喜歡的話,也ok。
3. concept的本質
concept本質上是:一個可以套用在模板上的constexpr bool
值。
相信下面這段程式碼能幫助你更好的理解它:
template <typename T>
concept Con = std::is_floating_point_v<T>;
int main(int argc, char* argv[]) {
static_assert(std::is_same_v<decltype(Con<float>), bool>);
std::cout << Con<int> << std::endl;
return 0;
}
顯然,上面的程式碼我們用is_same_v
確定了concept
生成的型別就是bool
型別。而同樣的,在執行期,我們們也可以將concept
的結果作為一個bool
常量進行使用,並列印。
所以,take it easy。 concept
很簡單,它只是C++20給你提供的一個better的工具,來擺脫被瘋狂的模板報錯所支配的恐懼。但即使你完全不瞭解它,使用老的方式,依然能夠同樣解決問題。
4.小結
C++的一些模板推斷的錯誤常常讓人抓狂。而很多時候我們使用它需要
- 要進行模板推斷型別的程式設計設計
- 利用SFINAE的方式來型別約束
這無形之中增加Coding時的心智成本,而concept
作為一個新的語法糖,給了我們拆分二者的機會:讓上帝歸上帝,凱撒歸凱撒。
使用好concept
來進行型別約束,enjoy新標準帶來的便利吧。
希望大家能夠有所收穫,筆者水平有限。成文之處難免有理解謬誤之處,歡迎大家多多討論,指教。