摘要訊息 : 一些 C++ 17 引入的小特性.
0. 前言
C++ 17 引入的絕大多數重要特性我們基本都介紹完了, 可以在 Jonny'Blog 中搜尋 C++ 17 閱覽這些文章. 還有一些小特性也是 C++ 17 引入的, 本文文章將通過羅列的方式介紹一部分特性.
更新紀錄 :
- 2022 年 6 月 7 日進行第一次更新和修正.
1. 函式型別增強
雖然 C++ 11 引入了 noexcept
標識, 但是它並沒有影響函式的型別. 也就是在 C++ 11 和 C++ 14 中, void () noexcept
和 void ()
是一樣的型別. C++ 17 提案 P0012R1《Make exception specifications be part of the type system》提出讓例外標識稱為型別系統的一部分. 因此自 C++ 17 起, void () noexcept
和 void ()
就不再是一樣的型別了.
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 new
和 operator delete
:
Code 4. 和記憶體對位有關的 operator new 和 operator deletevoid *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
表達式替代.
自創文章, 原著 : Jonny. 如若閣下需要轉發, 在已經授權的情況下請註明本文出處 :