摘要訊息 : 一些 C++ 17 引入的小特性.

0. 前言

C++ 17 引入的絕大多數重要特性我們基本都介紹完了, 可以在 Jonny'Blog 中搜尋 C++ 17 閱覽這些文章. 還有一些小特性也是 C++ 17 引入的, 本文文章將通過羅列的方式介紹一部分特性.

更新紀錄 :

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

1. 函式型別增強

雖然 C++ 11 引入了 noexcept 標識, 但是它並沒有影響函式的型別. 也就是在 C++ 11 和 C++ 14 中, void () noexceptvoid () 是一樣的型別. C++ 17 提案 P0012R1《Make exception specifications be part of the type system》提出讓例外標識稱為型別系統的一部分. 因此自 C++ 17 起, void () noexceptvoid () 就不再是一樣的型別了.

2. 使用 auto 推導非型別樣板引數

auto 自 C++ 98 就存在, 當時它還是從 C 繼承過來的. 在 C++ 11 之後, auto 有了新的用途, 舊的用途被移除. 而在 C++ 14 之後, auto 被允許用於泛型 Lambda 表達式. 於是自 C++ 14 之後, auto 有了佔位的用途. C++ 17 提案 P0127R2《Declaring non-type template parameters with auto提出, 讓非型別樣板參數也支援使用 auto 進行推導 :

#include <iostream>

template <auto Value>
int x = 0;
template <>
int x<1> = 1;       // Value 被推導為 int
template <>
int x<2l> = 2;      // Value 被推導為 long

template <auto Value, auto ...Values>
struct value_list : value_list<Values...> {
    constexpr static auto value {Value};
    using type = value_list<Values...>;
};
template <auto Value>
struct value_list<Value> {
    constexpr static auto value {Value};
    using type = value_list<Value>;
};

using namespace std;
int main(int argc, char *argv[]) {
    using list = value_list<1, 2, 3>;
    cout << list::value << endl;        // 輸出 : 1
    cout << list::type::value << endl;      // 輸出 : 2
    cout << list::type::type::value << endl;        // 輸出 : 3
    cout << list::type::type::type::value << endl;      // 輸出 : 3
    cout << x<0> << endl;       // 輸出 : 0
    cout << x<1> << endl;       // 輸出 : 1
    cout << x<2> << endl;       // 輸出 : 0
    cout << x<2l> << endl;      // 輸出 : 2
}

然而, 這樣會導致樣板樣板參數出現一些問題 :

#include <iostream>

template <template <auto> typename>
void f_auto() {
    std::cout << "auto" << std::endl;
}
template <template <int> typename>
void f_int() {
    std::cout << "int" << std::endl;
}

template <auto>
struct s_auto {};
template <int>
struct s_int {};

int main(int argc, char *argv[]) {
    f_auto<s_int>();        // ???
    f_int<s_auto>();        // ???
    f_auto<s_auto>();       // 輸出 : auto
}

在這種情況下, auto 如何推導成了問題, 從而導致編碼錯誤. C++ 17 提案 P0522R0《Matching of template template-arguments excludes compatible templates》提出讓這種情況通過編碼, 從而 f_auto<s_int>(); 的輸出為 auto, f_int<s_auto>(); 的輸出為 int.

除此之外, 對於帶有預設引數的多個樣板參數, P0522R0 還提出讓以下程式碼通過編碼 :

template <template <typename> typename> void f();
template <typename, typename = void>
struct s;
int main(int argc, char *argv[]) {
    f<s>();     // C++ 17 之前必定會產生編碼錯誤, 但是 P0522R0 希望此通過編碼
}

但是 Clang 對此並不支援. 在 GCC 下, 上述程式碼可以通過編碼.

3. 帶有記憶體對位的記憶體配置函式

C++ 11 為類別引入了記憶體對位限定 alignas, 但是並沒有引入與之對應的正確記憶體配置方式. 為此, C++ 17 提案 P0035R4《Dynamic memory allocation for over-aligned data》提出在名稱空間 std 中增加了一個限制作為範圍的列舉型別 : enum class std::align_val_t : std::size_t {};. 在標頭檔 <memory> 增加了若干個和記憶體對位有關的 operator newoperator delete :

void *operator new(std::size_t, std::align_val_t);
void *operator new[](std::size_t, std::align_val_t);
void operator delete(void *, std::align_val_t);
void operator delete[](void *, std::align_val_t);
void operator delete(void *, std::size_t, std::align_val_t);
void operator delete[](void *, std::size_t, std::align_val_t);
void *operator new(std::size_t, std::align_val_t, const std::nothrow_t &) noexcept;
void *operator new[](std::size_t, std::align_val_t, const std::nothrow_t &) noexcept;
void operator delete(void *, std::align_val_t, const std::nothrow_t &) noexcept;
void operator delete[](void *, std::align_val_t, const std::nothrow_t &) noexcept;
void operator delete(void *, std::size_t, std::align_val_t, const std::nothrow_t &) noexcept;
void operator delete[](void *, std::size_t, std::align_val_t, const std::nothrow_t &) noexcept;

4. 從型別包中繼承

#include <iostream>

template <typename ...Fs>
struct overloader : ... {
    // 將 Fs... 中的函式呼叫運算子繼承過來
};

template <typename ...Fs>
constexpr auto make_overloader(Fs &&...f) {
    return overloader<Fs...> {forward<Fs>(f)...};
}

int main(int argc, char *argv[]) {
    auto o {make_overloader([](const auto &a) {
        std::cout << "auto : " << a << std::endl;
    }, [](float f) {
        std::cout << "float : " << f << std::endl;
    })};
    o(1.0f);        // 輸出結果 : float : 1
    o(2);       // 輸出結果 : auto : 2
}

如何補充 Code 5-1 才能通過編碼並且得到我們期望的輸出? 在 C++ 17 之前, 並不能直接使用 C++ 11 引入的 using 繼承, 需要遞迴地進行實作 :

#include <iostream>

template <typename F, typename ...Fs>
struct overloader : F, overloader<Fs...> {
    using F::operator();
    using overloader<Fs...>::operator();
};
template <typename F>
struct overloader<F> : F {
    using F::operator();
};

template <typename ...Fs>
constexpr auto make_overloader(Fs &&...f) {
    return overloader<Fs...> {forward<Fs>(f)...};
}

int main(int argc, char *argv[]) {
    auto o {make_overloader([](const auto &a) {
        std::cout << "auto : " << a << std::endl;
    }, [](float f) {
        std::cout << "float : " << f << std::endl;
    })};
    o(1.0f);        // 輸出結果 : float : 1
    o(2);       // 輸出結果 : auto : 2
}

C++ 17 提案 P0195R2《Pack expansions in using-declarations》提出允許 using 宣告中直接將型別包中的需要使用的函式繼承過來 :

#include <iostream>

template <typename ...Fs>
struct overloader : Fs... {
    using Fs::operator()...;
};

template <typename ...Fs>
constexpr auto make_overloader(Fs &&...f) {
    return overloader<Fs...> {forward<Fs>(f)...};
}

int main(int argc, char *argv[]) {
    auto o {make_overloader([](const auto &a) {
        std::cout << "auto : " << a << std::endl;
    }, [](float f) {
        std::cout << "float : " << f << std::endl;
    })};
    o(1.0f);        // 輸出結果 : float : 1
    o(2);       // 輸出結果 : auto : 2
}

5. 重新定義廣義左值和純右值

對於不可移動類別, 在 C++ 17 之前僅能通過動態記憶體配置來編寫工廠函式 :

struct non_moveable {
    non_moveable(non_moveable &&) = delete;
    non_moveable &operator=(non_moveable &&) = delete;
    //...
};
non_moveable make();        // 會產生複製建構, 而並非是原地建構
non_moveable *make();       // 省略一次複製建構, 但是使用了動態記憶體配置

除此之外, 如果允許原地建構的話, Code 6 還可以為 make 函式增加 noexcept 標識以方便編碼器進行優化. 對於動態記憶體配置版本的 make 函式, 一般來說不能為其增加 noexcept 標識.

對於大型物件, 有時候, 移動帶來的消耗可能比重新建構更多. 使用 auto 宣告變數有時候可能無法避免移動, 即使編碼器實際上不會呼叫移動操作. 結合 Code 6 和這兩個問題, 都是目前 C++ 針對複製或者移動省略規範的不足, C++ 17 提案 P0135R0《Guaranteed copy elision through simplified value categories》 提出重新定義廣義左值和純右值的定義, 使得上面這些情況得到解決, 並且會讓 non_moveable 對應的非動態記憶體配置版本的 make 函式通過編碼, 並且進行原地建構而非使用動態記憶體配置.

6. 移除動態例外情況

C++ 98 引入了動態例外情況 : throw(type...). 在 C++ 11 發布時, 例外情況的重要性已經得到絕大多數人的認可, 但是動態例外情況一直被認為是毫無用處的, 並且它還被認為是一個失敗的實驗.

儘管移除了 throw(type...) 這樣的動態例外情況標識, 但是仍然保留了 throw() 這樣的標識, 並作為 noexcept(true) 的別名以相容舊的程式碼. 但是, throw() 並不建議使用, 而是應該由 noexcept 或者 noexcept 表達式替代.