摘要訊息 : 一些 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 進行推導 :

Code 1. 使用 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 }

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

Code 2. auto 與一般非型別參數的衝突
#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 還提出讓以下程式碼通過編碼 :

Code 3. 帶有預設引數的多個樣板參數
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 :

Code 4. 和記憶體對位有關的 operator new 和 operator 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. 從型別包中繼承

Code 5-1. 繼承函式呼叫運算子
#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 繼承, 需要遞迴地進行實作 :

Code 5-2. C++ 17 之前的解決方案
#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 宣告中直接將型別包中的需要使用的函式繼承過來 :

Code 5-3. C++ 17 中的解決方案
#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 之前僅能通過動態記憶體配置來編寫工廠函式 :

Code 6. 工廠模式
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 表達式替代.