摘要訊息 : C++ 14 Proposal N3638《Return type deduction for normal functions》導讀

C++ 14 Proposal N3638《Return type deduction for normal functions》導讀

C++ 11 引入了尾置回傳型別, 但是大神們覺得這還不太夠, 他們希望 C++ 直接可以支援函式回傳型別推導. N3638 提出了這樣的需求. 由於之後的文章需要用到回傳型別推導的特性, 所以我提前發布這篇文章

不論在名稱空間中還是在類別之下, C++ 14 都支援了函式回傳型別推導, 也就是說之前我們寫的程式碼 :

emplate <typename T, typename U>
auto add(const T &x, const U &y) -> decltype(x + y);

在 C++ 14 下, 可以直接省略尾置回傳型別 :

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

我們所說的都是比較簡略的情況, 這篇 Paper 中提到了一些比較複雜的情況

當我們宣告一個函式的時候, 若回傳型別使用 auto, 那麼在實作函式的時候回傳型別也必須為 auto :

auto f();
auto f() {
    return 0;
}     //OK
int f();        //Error

當函式的回傳型別需要推導, 但是函式在呼叫的時候無法推導得到回傳型別的時候, 會產生編碼錯誤 :

auto f();
int main(int argc, char *argv[]) {
    int a {f()};        //Error
}

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

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

若函式存在多個 return 陳述式, 那麼每一個 return 陳述式回傳的物件其型別應該一致, 否則會產生編碼錯誤 :

auto f(int a) {
    if(a >= 10) {
        return 1;
    }
    return false;       //Error
}

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

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

對於回傳型別為 auto & 的函式, 若函式實作中沒有回傳任何物件, 那麼將會產生編碼錯誤 :

auto &f() {}        //Error

lambda 表達式也支援了回傳型別推導 :

auto lambda {[]() -> auto {
    //...
}};

回傳型別推導不可以用於虛擬函式 :

class Foo {
public:
    virtual auto Foo() {}       //Error
};

return 陳述式中出現了初始化列表, 那麼不進行推導 :

auto f {
    return {1, 2, 3};       //Error
};

但是若要回傳一個 std::initializer_list, 仍然可以通過宣告一個推導為 std::initializer_list 的變數, 然後回傳這個變數來實現 :

auto f() {
    auto list = {1, 2, 3};
    return list;     //OK
};

這就是 Paper N3922 提出修改 auto 的推導規則的原因之一. 因為 std::initializer_list 的生存週期僅限於函式 f 之內, 如果回傳它, 就相當於回傳一個陣列 :

auto f() {
    int arr[] {1, 2, 3};
    return arr;     //使用 arr 中的任意值都會有未定行為
};

除了這些之外, Paper 中還提到了 decltype(auto), 這個我們之前已經說過了, 就不再重複了

實際上我並不推薦使用這個特性, 特別是針對程式庫作者而言, 因為用戶在閱讀程式碼的時候, 看到 auto 可能會被疑惑 : 這個函式到底回傳什麼? 假如某個函式有兩百行之多, 那麼我想有些人看到函式的回傳型別需要推導, 可能會直接放棄使用這個程式庫. 當然, 這個特性也不是毫無用武之地的, 文章開頭給出了一個兩個值相加的實例. 事實上, 可能會出現這種情況 :

template <typename Fx, typename Gx>
Hx add(Fx f, Gx g);

我們並不知道 FxGx 相加會產生什麼樣的結果, 所以 Hx 可能是未知的, 需要手動給定 :

template <typename Hx, typename Fx, typename Gx>
Hx add(Fx f, Gx g);

這樣, 函式 add 在呼叫的時候需要指定第一個樣板參數. 然而, 在 C++ 11 中我們可以使用 decltype 和尾置回傳型別 :

template <typename Fx, typename Gx>
auto add(Fx f, Gx g) -> decltype(f + g);

在 C++ 14 中運用回傳型別推導這個特性, 可以直接省略 decltype :

template <typename Fx, typename Gx>
auto add(Fx f, Gx g);

我再用另外一個實例說明. 設有一引數包 ...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'

對於 lambda 表達式來說, 其實 C++ 11 就已經支援了回傳型別推導, 只要省略掉 lambda 表達式的尾置回傳型別即可. 在 C++ 14 中, 只不過將回傳型別使用 auto 佔位後強行要求編碼器進行推導罷了