潮.C++20 | Concepts 之縮寫函式模板 - 參數全面 auto 的時代來了!

TJSW
11 min readMay 15, 2021

--

Photo by NII on Unsplash

C++11 推出多年之後,同學們就算什麼都不懂至少會用最開心的語法糖:宣告變數時 auto 自動推導型別。C++20 讓大家放飛自我,更多地方無腦寫 auto,讓同學們的創造力突破天際沒有極限!不過這東西還是有個官方文雅的名字的,今天就來談談 C++20 的 abbreviated function template 縮寫函式模板,還有他和 C++20 的 Concepts 這個新功能之間的糾葛。

長什麼樣子

同學們看到標題一定迫不及待想知道無腦參數寫 auto怎麼寫,不賣關子啦,直接上代碼,讓我們把兩個變數相加

auto sum(auto a, auto b) {
return a + b;
}
int main()
{
int ia = 10, ib = 3;
double da = 55., db = .66;
std::string sa = "得", sb = "第一";
std::cout
<< sum(ia, ib) << '\n' // 13
<< sum(da, db) << '\n' // 55.66
<< sum(sa, sb) << '\n'; // 得第一
}

是的沒錯!我只寫了一個 auto sum(auto a, auto b),就能夠把 intdoublestd::string 都拿來相加,而且結果就是依據各個型別的相加規則運算。以前要寫三個 sum() 現在只要寫一個,簡直就跟隔壁棚 JavaScript 一樣美好的世界,怎麼寫怎麼動 (誤),但各位同學千萬注意並不是代表 C++ 變成動態語言惹😂。

這裡一連出現三個 auto,都和 C++11 會出現的位置不太一樣,但意涵是類似的:就是自動型別推導。

  • auto a, auto b:編譯時依照傳入參數自動推導猜出 ab 的型別
  • 回傳值的 auto:依據 a + b 表達式的結果型別決定回傳什麼東西

此次 C++20 新增的功能主要是前者。不過「依照傳入參數自動推導」這件事情是不是有點眼熟呢,難道是…!?

函式模板 function template。我們又見面了

和函式模板什麼關係

C++20 這次新增加的語法,用 auto 代表依照傳入參數自動猜型別,其實就只是函式模板 function template 的語法糖。也就是說以前同學們在學校要寫出上面泛型 (generic) 的 sum() 函式要這樣

// C++11
template<typename T, typename U>
auto sum(T t, U u) -> decltype(t + u) // 依照 t + u 推斷回傳型別
{
return t + u;
}

現在就如同前面章節那樣短小幹練一行解決,也就是說現在你只要在函式參數寫上 auto,那就等價於編譯器幫你發明一個型別模板參數寫在那兒。這也是這個語法糖的命名由來:縮寫函式模板 abbreviated function template。

很自然地大家在以前使用 auto 宣告變數時的修飾裝備也可以拿來用:auto& 接引用,const auto& 接暫存值,auto && 宇宙引用 (universal reference) 等等。

函式模板的特化 (template specialization)

我們的 sum() 他終究是個函式模板,所以過往函式模板能做的事情他都逃不了,我們接著就要對他來動手腳了。

首先就是對 sum() 來做個特化 (template specialization)。你各位一定看一看上面的 sum() 發現如果想要把兩個傳統的 C 字串 (const char* 型別) 加在一起會壞的面目全非;所以我們想對參數皆為 const char* 特化實作

auto sum(auto a, auto b) {
return a + b;
}
// 對 const char*, const char* 特化
template<>
auto sum(const char *a, const char *b) {
return std::string{} + a + b;
}
int main()
{
int ia = 10, ib = 3;
const char* sa = "TJ", *sb = "SW";
std::cout
<< sum(ia, ib) << '\n' // 13
<< sum(sa, sb) << '\n'; // TJSW
}

特化版本的 sum(const char*, const char*) 先做出一個空的 std::string,再利用 std::string+ 去把 ab 連接起來,非常地滑順。

std::sort 的應用:模板手動給定型別參數

現在我們暫時不管我們的 sum(),先來看一個比較大小的函式 cmp()

bool cmp(auto a, auto b) { return a < b; }

一樣利用了縮寫模板的語法一行就讓任意能比較大小的物件都能丟進去。我們現在就把這東西拿過來套用在 std::sort 排序各種型別的陣列吧。

int iarr[] {5, 2, 3, 4, 1};
char carr[] {'t', 'j', 's', 'w'};
std::sort(iarr, iarr + 5, cmp<int, int>); // 1 2 3 4 5
std::sort(carr, carr + 4, cmp<char, char>); // j s t w

因為 cmp 說穿了就是函式模板而已,所以要使用他也可以以手動給定參數型別的方式讓 std::sort() 吃它需要的比較大小函式來排序陣列。

如同學們想說那我能不能直接給 std::sort(arr, arr+N, cmp);?這樣是不行的,因為前面提過 cmp 這樣寫著就是函式模板,並不是一個可以呼叫的函式或物件 (Callable)。這也是 C++20 縮寫函式模板和 generic lambda 最大的不同之處。

和 Concepts 也能相互糾纏

是的,如果大概了解 C++20 Concepts 的同學應該知道,C++20 的模板和 Concepts 緊密不可分,我認為和 Concepts 的連結才是縮寫函式模板的最精華的語法糖部分,沒看過 Concepts 的同學可以先到這邊看看

上面的 sum 我們希望能在編譯時期 (compile time) 就判斷丟給 auto 的型別能不能相加,我們定義一個 concept 叫做 Summable,主要就想知道相加的表達式 484 合法的,而且加起來的型別不會變成別的東西。

template<typename T>
concept Summable = requires(T t) {
{ t + t } -> std::same_as<T>;
};

而要把 concept 加上去限制 auto 參數其實和 concept 限制模板參數的語法精神 87% 像。在函式模板同學們可能寫:

template<typename T, typename U>
auto sum(Summable T t, Summable U u) { // ... }

在縮寫函式模板這邊,讓我們跟前面針對 const char* 特化的 sum()合體起來,就可以這樣:

auto sum(Summable auto a, Summable auto b) {
return a + b;
}
auto sum(const char *a, const char *b) {
return std::string{} + a + b;
}
int main()
{
int ia = 10, ib = 3;
const char* sa = "TJ", *sb = "SW";
int *pia = &ia, *pib = &ib;
sum(ia, ib); // 13
sum(sa, sb); // TJSW
// sum(pia, pib); // compile-error
}

語法規定 Concept 的名字一定要緊緊接在 auto 的前面緊密連結,其他的 const 什麼的再放他的前面。這樣的限制 auto 的方式在 C++20 也有一個文雅的名字:constrained auto。

特別注意:這邊事實上並沒有模板特化 const char* 版本的 sum,因為我們其實沒有定義出 sum 的 template 原型,而是只有一個被 Summable 限制的 sum()。所以這兩個 sum 其實單純只是兩個同名稱的函式,互相只是以函式多載 (function overloading) 的方式被編譯器解析並呼叫。

把上面那行 sum(pia, pib) 的註解拿掉後,理所當然地編譯器根據 Summable 的定義來跟我們抱怨了:

3.1.cpp:10:10: note: because 'int *' does not satisfy 'Summable'
auto sum(Summable auto a, Summable auto b) {
^
3.1.cpp:7:7: note: because 't + t' would be invalid: invalid operands to binary expression ('int *' and 'int *')
{ t + t } -> std::same_as<T>;

不定長度模板 (Variadic Template)

沒錯,他是個模板,所以 C++11 的 variadic template 一定得來鬧一下,經過上面跟 concept 的緊密連結之後,同學們應該猜到搭配 variadic template 要怎麼寫這次的 auto 惹。

假設我們上面的 sum,除了包容萬物吃進來各種型別以外,還想要各種長度都能進來🤫。

結論就是跟 variadic template 的精神一樣後面加上點點點變成 auto...

auto sum(auto&&... args) {
return ( ... + args );
}
int main()
{
std::string sa = "a", sb = "bc", sc = "def";
sum(1, 2, 3, 4, 5); // 15
sum(sa, sb, sc); // abcdef
}

這邊借用了 C++17 的 fold expression,要把 int 還是 std::string,要 3 個還是 5 個,簡單暴力一個函式就給他加在一起。

大雜燴:Variadic template + Concept constraint

同學們學習了 1+1 之後,課本總是會給同學們一個變化題,這邊給同學帶來一個大雜燴。目標很清楚簡單:讓 concept 的限制也能在縮寫函式模板的語法下套用到 variadic template 上。

  • 不定個數參數的相加表達式要合法,而且是一個 arithmetic 型別
  • 針對 const char* 一樣要特化多載,也支援任意個字串相加

所謂的 arithmetic 可運算型別是這個意思

template<typename T>
concept IsAri = std::is_arithmetic_v<T>;

不知道是什麼同學直接理解為這個限定 intdouble 就好 🤣。我們上面的 sum 就爆改一下,前面是將 concept 名字寫在 auto 前面,這邊改成寫在 auto... 的前面

template<typename... Args>
concept Summable = requires (Args... args) {
( ... + args );
requires ( IsAri<Args> && ... );
};
auto sum(Summable auto... args) {
return ( ... + args );
}

針對 const char* 的版本使用了標準庫裡面的 std::same_as<> concept 限定 args... 參數包裡面所有人必須和 const char* 一樣,並且再度借用了 fold expression 把他全部接起來

auto sum(std::same_as<const char*> auto... args) {
return ( std::string{} + ... + args );
}

最終我們呼叫的結果就會如我們想像的

const char* sa = "Bambi", *sb = "Candy", *sc = "UN";
int *pia, *pib;
sum(1, 2, 3, 4, 5); // 15
sum(sa, " ", sb); // Bambi Candy
sum(sc, " Village"); // UN Village
// sum(pia, pib); // compile-error

講完了

其實 abbreviated function template 算是 C++20 的新功能裡面比較容易被遺忘的遺珠,大家都會把目光放在介紹 concept 的緣由,語法怎麼寫,可以做到哪些限制等等,但因為他只是跟著 Concepts 的幾篇提案裡面提出的一個小篇章語法改進,容易常常沒被人拿出來介紹,目的主要就是上面提到的 constrained auto,讓 concept 更容易被套用在函式上面。

但他也是讓現代 C++ 語法更好寫,閱讀更加方便的語法糖。不用寫一堆 template<typename> 的重複的東西也可以讓大家容易讀懂說:喔~這邊是任意型別都可能進來的。今天就到這邊啦,大家掰掰

--

--