摘要訊息 : C++ 14 中, 函式的回傳型別也可以讓編碼器來推導了.

0. 前言

C++ 11 引入了尾置回傳型別, 但是實際上編碼器是有能力並且完全可以從 return 陳述式中去推導函式的回傳型別.

C++ 14 提案 N3638《Return type deduction for normal functions》就提出了讓編碼器從 return 陳述式中去推導函式的回傳型別, 使得尾置回傳型別可以省略.

更新紀錄 :

  • 2022 年 5 月 12 日進行第一次更新和修正.

1. 省略尾置回傳型別

不論是普通的函式還是類別中的成員函式, 它們在 C++ 14 中都支援使用 auto 來推導回傳型別 :

template <typename T, typename U>
auto add(const T &x, const U &y);

struct Foo {
    auto f();
};
auto Foo::f() {}

2. 複雜情形

函式在僅當我們宣告一個函式的時候, 若回傳型別使用 auto, 那麼在實作函式的時候回傳型別也必須為 auto. 如果產生宣告和實作時不一致, 那麼就會產生編碼錯誤. 這個規則對普通函式或者成員函式都成立. 當函式的回傳型別需要推導, 但是函式在呼叫的時候無法推導得到回傳型別的時候, 會產生編碼錯誤. 這分為兩類情況, 一種是在函式呼叫處無法找到函式的定義, 只能看到函式的宣告; 另一種是存在模稜兩可的回傳型別, 例如函式中不同的 return 陳述式的回傳值型別無法統一.

函式樣板在特製化的時候, 若原函式樣板的回傳型別需要推導, 那麼特製化而來的函式的回傳型別也必須經過推導 :

template <typename T>
auto f();
template <>
auto f<char>();     // OK
template <>
int f<int>();       // Error

對於遞迴來說, 回傳型別同樣可以推導, 但是以下的遞迴無法進行推導 :

auto f() {
    return f();     //Error
}

對於回傳型別為 auto & 的函式, 若函式實作中沒有回傳任何物件, 那麼將會產生編碼錯誤. 在成員函式中, 如果某個函式是虛擬函式, 那麼它不能使用 auto 推導回傳型別. 當 return 陳述式中出現了初始化列表, 那麼編碼器會拒絕推導, 從而產生編碼錯誤 (這就是 C++ 17 提案 N3922 提出修改 auto 的推導規則的原因之一. 因為 std::initializer_list 的生存週期僅限於函式之內, 如果回傳它, 就相當於回傳一個函式之內的局域物件).

除了函式之外, Lambda 表達式也支援使用 auto 推導回傳型別了. 雖然在 C++ 11 中, Lambda 表達式可以通過省略回傳型別來讓編碼器進行推導, 但是那個推導規則並不完善.

N3638 中還提到了 decltype(auto), 這個在文章《【C++】autodecltype中已經介紹過了, 此處不再累贅.

3. 評價

實際上我並不推薦使用這個特性, 特別是針對程式庫作者而言, 因為用戶在閱讀程式碼的時候, 看到 auto 可能會被疑惑 : 這個函式到底回傳什麼? 假如某個函式有兩百行之多, 那麼我想有些人看到函式的回傳型別需要推導, 可能會直接放棄使用這個程式庫. 當然, 這個特性也不是毫無用武之地的, 第 1 節中給出了一個兩個值相加的實例, 我再用另外一個實例說明. 設有一引數包 args, 每一個引數對應的型別都支援比較操作. 我們需要對裡面的引數進行排序, 然後將其按順序放進 std::tuple 然後回傳, 那麼這個回傳型別就完全未知. 因為 std::tuple 中的樣板引數完全由引數包排序之後的結果來決定, 一定位置的樣板引數對應相應的排序後的值. 這個函式可以宣告為 :

template <typename ...Args>
auto sort(Args &&...args);

若函式呼叫 sort(1, 'a', 9.9f, 0.0, U'l'); 最後得到的物件應該是 : std::tuple<double, int, flost, char, char32_t>, 其中存儲了 0.0, 1, 9.9f, 'a'U'l'.