跨越十年的C++演進系列,分為5篇,本文為第四篇,后續會持續更新C++23~
前3篇如下:
跨越十年的C++演進:C++11新特性全解析
跨越十年的C++演進:C++14新特性全解析
跨越十年的C++演進:C++17新特性全解析
C++20標準是C++語言的第四個正式標準,于2020年12月正式發布。
首先先上C++20特性思維導圖:
接下來將從關鍵字、語法、宏、屬性、棄用這5個類目來講解~
1、關鍵字
1.1、concept
編譯器版本:GCC 10
concept?是 C++20 引入的重要特性之一,尤其適用于模板庫的設計與開發。其功能類似于 C# 中的泛型約束,但相比而言更為靈活和強大。
concept?允許我們為模板參數定義具有特定條件的類型約束。
示例:數值類型的約束
#include?<type_traits>
// 定義一個名為 number 的 concept,用于約束模板參數 T 必須是算術類型
template<typename?T>
concept?number = std::is_arithmetic_v<T>;
// 使用該 concept 來限制函數模板的參數類型
template<number T>
void?func(T t)
{ }
// 調用示例
func<int>(10); ? ? ? ?// 合法,int 屬于算術類型
func<double>(20.0); ??// 合法,double 屬于算術類型
struct?A?{ };
func<A>(A()); ? ? ? ??// 非法,A 不是算術類型,編譯失敗
1.2、requires
編譯器版本:GCC 10
僅使用?concept?還不足以完全發揮其潛力,真正使其強大的是?requires?表達式。通過結合?concept?和?requires,可以對模板參數進行更細粒度的限制,包括檢查成員變量、成員函數及其返回值等。
示例:約束類型必須具備某些成員函數或行為
#include?<type_traits>
template<typename?T>
concept?can_run =?requires(T t)
{std::is_class_v<T>; ? ? ? ? ? ? ? ? ? ??// 類型 T 必須是一個類或結構體t(); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 必須支持無參調用(括號運算符重載)t.run(); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 必須包含 run 成員函數std::is_same_v<decltype(t.run()),?int>;?// run 函數返回類型必須為 int
};
// 使用該 concept 的函數模板
template<can_run T>
int?func(T t)
{t();return?t.run();?// 可以直接返回 run() 的結果,因為其返回類型已被限定為 int
}
func<int>(10);?// 錯誤,int 不是 class 或 struct
struct?A?{void?run()?{ }
};
func<A>(A());?// 錯誤,缺少括號運算符重載
struct?B?{void?operator()()?{ }
};
func<B>(B());?// 錯誤,缺少 run 函數
struct?C?{void?operator()()?{ }void?run()?{ }
};
func<C>(C());?// 錯誤,run 返回類型不是 int
struct?D?{int?operator()()?{ }int?run()?{?return?0; }
};
func<D>(D());?// 正確,滿足所有條件
1.3、typename
編譯器版本:GCC 9
typename?在模板中主要有兩個用途:一是作為模板參數聲明;二是明確告訴編譯器某個嵌套名稱是一個類型名。在早期版本中,為了避免歧義,需要頻繁使用?typename?來輔助編譯器解析。而新版本增強了類型推導能力,使得部分場景下可以省略?typename。
例如,在只能推導出類型的上下文中,可不加?typename。
示例:
// 函數返回類型位于全局作用域,只可能是類型,因此無需 typename
template<class?T> T::R?f();?// 合法
// 作為函數參數時則需要顯式指定為類型
template<class?T>?void?f(T::R);?// 非法,無法推斷為類型
template<typename T>
struct PtrTraits {using?Ptr?=?T*;
};
template<class?T>
struct S {using?Ptr?=?PtrTraits<T>::Ptr;?// 合法,在 defining-type-id 上下文中T::R?f(T::P p)?{return?static_cast<T::R>(p);?// 合法,在函數體內也可識別為類型}auto?g()?-> S<T*>::Ptr;?// 合法,尾隨返回類型
};
template<typename T>
void?f()?{void?(*pf)(T::X); ? ??// 合法,pf 是指向函數的指針void?g(T::X); ? ? ? ??// 非法,T::X 無法被解釋為類型
}
1.4、explicit
編譯器版本:GCC 9
新增于 C++11 版本,具體可參考 C++11 新特性相關內容。
C++20 中擴展了?explicit,允許傳入一個布爾值來控制是否啟用顯式構造行為。
示例:
struct?A {explicit(false) A(int) { }?// 允許隱式轉換
};
struct?B {explicit(true) B(int) { }?// 禁止隱式轉換
};
A a =?10;?// 合法,允許隱式構造
B b =?10;?// 非法,禁止隱式構造
1.5、constexpr
編譯器版本:GCC 9
該特性最初在 C++11 中引入,如需詳細了解可參考 C++11 的新特性。
① 在 C++20 中,constexpr?的使用范圍得到了進一步擴展,新增了對虛函數的支持。其用法與普通函數一致,無需額外說明。
②?constexpr?函數中不再允許使用?try-catch?語句塊。此限制也適用于構造函數和析構函數中的異常處理邏輯。
1.6、char8_t
編譯器版本:GCC 9
char8_t?是為 UTF-8 編碼專門設計的新類型。今后,UTF-8 字符字面量將由?char8_t?類型接收,而不再是?char?類型。
目前 GCC 編譯器對該特性的支持尚未完全實現,相關內容仍在持續完善中。
1.7、consteval
編譯器版本:GCC 11
consteval?關鍵字用于定義“立即函數”,這類函數必須在編譯期執行完畢,并返回一個編譯期常量結果。函數參數也必須是能夠在編譯期確定的值,且函數內部的所有運算都必須能在編譯期完成。
相較于?constexpr,consteval?對函數的限制更加嚴格。constexpr?函數會根據傳入參數是否為常量表達式自動決定是在編譯期還是運行期執行;而?consteval?函數則強制要求所有調用都必須發生在編譯期。
示例代碼:
#include?<iostream>
constexpr?int?f(int?a)
{return?a * a;
}
// 參數a必須是編譯期常量
consteval?int?func(int?a)
{return?f(a);?// 合法,因為f()可以在編譯期計算
}
int?main()
{int?a;std::cin >> a;int?r1 =?f(a); ? ? ? ? ? ??// 合法,a是運行期變量,f將在運行期執行int?r2 =?func(a); ? ? ? ? ?// 錯誤,a不是編譯期常量int?r3 =?func(1000); ? ? ??// 合法,1000是常量int?r4 =?func(f(10)); ? ? ?// 合法,f(10)在編譯期完成,符合consteval要求return?0;
}
1.8、co_await、co_yield、co_return
編譯器版本:GCC 10
編譯選項:-fcoroutines
協程三大關鍵字:co_await、co_yield?和?co_return。
1.8.1、語法示例
(先了解基本語法,后文將詳細解釋)
using?namespace?std::chrono;
struct?TimeAwaiter?
{std::chrono::system_clock::duration duration;bool?await_ready()?const?{?return?duration.count() <=?0; }void?await_resume()?{}void?await_suspend(std::coroutine_handle<> h)?{}
};
template<typename?_Res>
struct?FuncAwaiter?
{_Res value;bool?await_ready()?const?{?return?false; }_Res?await_resume()?{?return?value; }void?await_suspend(std::coroutine_handle<> h)?{ std::cout << __func__ << std::endl; }
};
TimeAwaiter?operator?co_await(std::chrono::system_clock::duration d)?
{return?TimeAwaiter{d};
}
static?FuncAwaiter<std::string>?test_await_print_func()
{std::this_thread::sleep_for(1000ms);std::cout << __func__ << std::endl;return?FuncAwaiter<std::string>{std::string("emmmmmmm ") + __func__};
}
static?generator_with_arg?f1()?
{?std::cout <<?"11111"?<< std::endl;co_yield?1;?std::cout <<?"22222"?<< std::endl;co_yield?2;?std::cout <<?"33333"?<< std::endl;co_return?3;
}
static?generator_without_arg?f2()?
{?std::cout <<?"44444"?<< std::endl;std::cout <<?"55555"?<< std::endl;std::cout <<?"66666"?<< std::endl;co_return;
}
static?generator_without_arg?test_co_await()?
{std::cout <<?"just about go to sleep...\n";co_await?5000ms;std::cout <<?"resumed 1111\n";std::string ret =?co_await?test_await_print_func();
}
總結:
- co_return [result]
表示協程最終的返回結果。若未指定值,則默認為?void。 - co_yield value
表示協程掛起時返回的值。不可省略,且必須與?co_return?返回類型一致。當?co_return?返回類型為?void?時,不能使用?co_yield。 - co_await value
- 可以被重載,重載函數應返回一個?awaiter。
- 若?value?是某個函數調用,則該函數必須返回一個?awaiter。
1.8.2、awaiter 說明
一個合法的?awaiter?類型必須實現以下三個接口函數:
- await_ready()
首先被調用,返回一個布爾值。若為?true,表示操作已完成,繼續執行后續代碼;若為?false,則協程將被掛起,并進入?await_suspend()。 - await_suspend(h)
接收當前協程的句柄?h。返回類型可以是?void?或?bool。若返回?false,協程不會掛起;若返回?void,等效于返回?true,即協程掛起。 - ?std::coroutine_handle<>?是標準庫提供的類型,用于引用協程對象,控制其生命周期和喚醒行為。
- await_resume()
當協程恢復執行時調用。其返回值就是?co_await?表達式的返回值。
1.8.3、協程函數
協程函數的返回類型必須包含一個名為?promise_type?的嵌套類型。這個?promise_type?負責管理協程的狀態和返回值。
編譯器會自動調用?promise_type::get_return_object()?來獲取協程函數的返回值(通常是?generator?類型),用戶無需手動編寫?return?語句。
通常情況下,generator?類型需要保存協程的句柄,以便外部程序控制協程的執行流程。
1.9、constinit
編譯器版本:GCC 10
constinit?用于確保變量在編譯期完成初始化,禁止動態初始化。
適用條件:
變量必須具有靜態存儲周期或線程局部存儲周期(thread_local)。thread_local?變量可以選擇不初始化。
示例代碼:
const?char?*?get_str1()
{return?"111111";
}
constexpr?const?char?*?get_str2()
{return?"222222";
}
const?char?*hahah =?" hhahahaa ";
constinit?const?char?*str1 =?get_str2();?// 合法,使用 constexpr 函數初始化
constinit?const?char?*str2 =?get_str1();?// 非法,使用非 constexpr 函數初始化
constinit?const?char?*str3 = hahah; ? ? ?// 非法,使用非常量表達式初始化
int?main()
{static?constinit?const?char?*str4 =?get_str2();?// 合法,靜態變量constinit?const?char?*str5 =?get_str2(); ? ? ? ?// 非法,非靜態/非 thread_localconstinit?thread_local?const?char?*str6; ? ? ? ?// 合法,thread_local 可不初始化return?0;
}
2、語法
2.1、位域變量的默認成員初始化
編譯器版本:GCC 8
C++20 允許在定義位域變量時為其指定默認初始值。這一特性提升了代碼的可讀性和安全性。
聲明語法格式如下:
類型 變量名 : 位數 = 初始值;
類型 變量名 : 常量表達式 {初始值};
示例:
int?a;
const?int?b =?1;
struct?S
{int?x1 :?8?=?42; ? ? ? ? ? ?// 合法,x1 是一個 8 位整型,并被初始化為 42int?x2 :?6?{42}; ? ? ? ? ? ?// 合法,同上,使用花括號初始化int?x3 :?true???10?: a =?20;?// 合法,三目運算結果是 10,未進行賦值操作(優先級問題)int?x4 :?true???10?: b =?20;?// 非法,b 是 const,不能對其賦值int?x5 : (true???10?: b) =?20;?// 合法,強制優先級后,位寬為 10,并初始化為 20int?x6 :?false???10?: a =?20;?// 非法,a = 20 不是常量表達式
};
2.2、修改 const 限定的成員指針
編譯器版本:GCC 8
在 C++20 中,允許對?.*?表達式中的第二個操作數進行更靈活的處理。特別是當該操作數是一個指向帶有?&?限定符的成員函數指針時,只有在其具有?const?限定的情況下才是合法的。
示例:
struct?S?{void?foo()?const&?{ }
};
void?f()
{S{}.foo(); ? ? ? ? ??// 合法,調用 const& 成員函數(S{}.*&S::foo)(); ? ?// C++20 起支持這種語法
}
2.3、允許 lambda 表達式按值捕獲 this
編譯器版本:GCC 8
lambda 表達式現在可以顯式地按值捕獲?this?指針,這使得 lambda 在捕獲對象狀態時更加清晰明確。
示例:
struct?S
{int?value;void?print(){auto f = [=,?this]() {this->value++;};}
};
上述代碼中,[=, this]?表示以值方式捕獲所有外部變量,并且顯式捕獲?this?指針。
2.4、指定初始化
編譯器版本:GCC 8
C++20 引入了類似 C99 的“指定初始化”語法,允許在構造聚合類型時通過字段名稱來初始化特定成員。但必須按照成員在類或結構體中定義的順序進行初始化。
示例:
struct?A {?int?x, y; };
struct?B {?int?y, x; };
void?f(A a,?int); ?// #1
void?f(B b, ...); ?// #2
void?g(A a); ? ? ??// #3
void?g(B b); ? ? ??// #4
void?h()
{f({.x =?1, .y =?2},?0); ? ? ? ??// 合法,調用 #1f({.y =?1, .x =?2},?0); ? ? ? ??// 非法,成員順序與定義不一致,無法匹配 #1f({.y =?1, .x =?2},?1,?2,?3); ??// 合法,調用 #2g({.x =?1, .y =?2}); ? ? ? ? ? ?// 非法,無法確定調用 #3 還是 #4
}
2.5、lambda 表達式支持模板
編譯器版本:GCC 8
C++20 支持在 lambda 表達式中使用模板參數,從而實現泛型 lambda。這一特性極大增強了 lambda 的靈活性和通用性。
示例 1:
int?a;
auto?f = [&a]<typename?T>(const?T &m) {a += m;
};
f(10);?// 正確,T 推導為 int
示例 2:
template<typename?T>
int?func(int?t)?
{return?t * t;
}
int?f()
{return?func<decltype([] {})>(20);?// 使用 lambda 類型作為模板參數
}
示例 3:
using?A =?decltype([] {});
void?func(A *)?{ }
func(nullptr);
template<typename?T>
using?B =?decltype([] {});
void?f1(B<int> *)?{ }
template<typename?T>
void?f2(B<T> *)?{ }
f1(nullptr); ? ? ? ? ?// 合法
f2<int>(nullptr); ? ??// 合法
2.6、從構造函數推導出模板參數類型
編譯器版本:GCC 8
C++20 允許在變量聲明時省略模板參數,編譯器將根據構造函數參數自動推導出實際類型。
示例:
vector v{vector{1,?2}};?// 合法,v 被推導為 vector<vector<int>>
tuple t{tuple{1,?2}}; ??// 合法,t 被推導為 tuple<int, int>
2.7、簡化 lambda 的隱式捕獲
編譯器版本:GCC 8
本節內容較為復雜,涉及 lambda 表達式捕獲機制的改進。由于篇幅限制,此處不再詳細展開。
如需深入了解,請參考提案文檔:
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0588r1.html
2.8、ADL與不可見的模板函數
編譯器版本:GCC 9
ADL(Argument-Dependent Lookup)是 C++ 中的一種機制,用于自動解析調用函數的位置,簡化代碼編寫。C++20 擴展了這一機制,使得它也可以應用于模板函數的推導。
示例:
int?h;
void?g();
namespace?N {struct?A?{};template<typename?T>?int?f(T);template<typename?T>?int?g(T);template<typename?T>?int?h(T);?// 注意這里的 h 是一個模板函數
}
int?x =?f<N::A>(N::A());?// 正確,調用了 N::f
int?y =?g<N::A>(N::A());?// 正確,調用了 N::g
int?z =?h<N::A>(N::A());?// 錯誤,因為全局命名空間中的 h 被認為是一個變量而非模板函數
2.9、operator<=>
由于篇幅限制,關于?operator<=>?的詳細討論請參考官方文檔:
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0515r3.pdf
2.10、基于范圍的 for 循環初始化
編譯器版本:GCC 9
C++20 引入了一種新的基于范圍的 for 循環語法,允許在循環開始前執行初始化語句。
新語法格式如下:
for([init-statement;]?for-range-declaration :?for-range-initializer) ...
示例:
int?a[] = {1,?2,?3,?4};
for(int?b =?0;?int?i : a) {// 使用 i 和 b 進行操作
}
2.11、默認可構造可分配的無狀態 lambdas
編譯器版本:GCC 9
C++20 允許獲取 lambda 表達式的類型,并創建該類型的對象,即使這個 lambda 沒有捕獲任何外部變量。
示例:
#include?<iostream>
#include?<map>
auto?greater = [](auto?x,?auto?y) {?return?x > y; };
std::map<std::string,?int,?decltype(greater)> map;
static?void?f()?{}
int?main()?{decltype(f) ff;ff();?// 調用靜態函數decltype(greater) d;d(10,?20);?// 調用比較操作return?0;
}
2.12、專門的訪問檢查
此特性已在 GCC 和 MSVC 編譯器中實現,但在其他編譯器中可能未完全支持。它允許在模板類內部忽略訪問權限來訪問另一個類的嵌套類。
示例:
class?A?{struct?impl1?{?int?value; };template<typename?T>?class?impl2?{ T value; };class?impl3?{?int?value; };
};
struct?B?{A::impl1 t;?// 錯誤:'struct A::impl1' 在此上下文中為私有
};
template<typename?T>
struct?trait?{A::impl1 t; ? ??// 正確A::impl2<T> t2;?// 正確void?func()?{A::impl1 tmp; ?// 正確tmp.value =?10;// 正確t2.value =?20;?// 正確A::impl3 t3; ??// 正確t3.value =?30;?// 正確}
};
int?main()?{trait<int> a;a.t.value =?10;?// 正確a.t2.value =?20;?// 錯誤:'int A::impl2<int>::value' 在此上下文中為私有return?0;
}
2.13、constexpr 函數的實例化
編譯器版本:GCC 9
當僅需獲取?constexpr?函數的返回值類型時,無需實例化整個函數,只需推導其返回類型即可。
示例:
template<typename?T>?
constexpr?int?f()?{?return?T::value; }
// 此處僅推導 f<T>() 的返回值類型
template<bool?B,?typename?T>?
void?g(decltype(B ? f<T>() :?0))?{ }
template<bool?B,?typename?T>?void?g(...)?{ }
// 因為需要實際獲取 int 類型的數據,所以必須實例化 f<T>()
template<bool?B,?typename?T>?void?h(decltype(int{B ? f<T>() :?0}))?{ }
template<bool?B,?typename?T>?void?h(...)?{ }
void?x()?{g<false,?int>(0);?// OK,因為不需要實例化 f<int>()h<false,?int>(0);?// 錯誤,即使 B 為 false,也需要實例化 f<int>()
}
2.14、允許 lambda 在初始化捕獲時進行包擴展
編譯器版本:GCC 9
C++20 擴展了包擴展的應用場景,現在可以在 lambda 初始化捕獲時使用包擴展。
示例:
#include?<functional>
template<class?F,?class... Args>
auto?invoke1(F f, Args... args)?{return?[f, args...]() ->?decltype(auto) {return?std::invoke(f, args...);};
}
template<class?F,?class... Args>
auto?invoke2(F f, Args... args)?{return?[f=std::move(f), ...args=std::move(args)]() ->?decltype(auto) {return?std::invoke(f, args...);};
}
template<class?F,?class... Args>
auto?invoke3(F f, Args... args)?{return?[f=std::move(f), tup=std::make_tuple(std::move(args)...)]() ->?decltype(auto) {return?std::apply(f, tup);};
}
2.15、放寬結構化綁定,新增自定義查找規則
編譯器版本:GCC 8
C++20 放寬了結構化綁定的限制,并允許自定義查找規則以適應更復雜的綁定需求。
自定義條件包括:
- 實現?get<int>(Type)?函數或?Type::get<int>()?成員函數。
- 特化?tuple_size?和?tuple_element?結構體。
- 確保?get<int>?返回路徑數量與?tuple_size?指定的數值一致。
- get<int N>
- ?函數返回類型應與?tuple_element?對應索引指定的類型相同。
示例 1:
#include?<string>
#include?<tuple>
struct?A?{int?a;int?b;
};
struct?X?:?private?A {std::string value1;std::string value2;
};
template<int?N>?
auto&?get(X &x)?{if?constexpr?(N ==?0)return?x.value2;
}
namespace?std {template<>?class?tuple_size<X> :?public?std::integral_constant<int,?1> {};template<>?class?tuple_element<0, X> {public:?using?type = std::string;};
}
int?main()?{X x;auto& [y] = x;?// y 的類型為 stringauto& [y1, y2] = x;?// 錯誤:提供了 2 個名稱進行結構化綁定,而 'X' 解構為 1 個元素return?0;
}
示例 2:
#include?<string>
#include?<tuple>
struct?A?{int?a;int?b;
};
struct?X?:?protected?A {std::string value1;std::string value2;template<int?N>?auto&?get()?{if?constexpr?(N ==?0)return?value1;else?if?constexpr?(N ==?1)return?a;}
};
namespace?std {template<>?class?tuple_size<X> :?public?std::integral_constant<int,?2> {};template<>?class?tuple_element<0, X> {public:?using?type = std::string;};template<>?class?tuple_element<1, X> {public:?using?type =?int;};
}
int?main()?{X x;auto& [y1, y2] = x;?// y1 為 string 類型,y2 為 int 類型return?0;
}
2.16、放寬基于范圍的 for 循環,新增自定義范圍方法
編譯器版本:GCC 8
在 C++20 中,允許不通過類內部的?begin()?和?end()?成員函數來實現基于范圍的 for 循環。現在可以將這兩個函數實現在類外部,依然可以被正確識別。
示例:
#include?<iostream>
struct?X?{int?a =?1;int?b =?2;int?c =?3;int?d =?4;int?e =?5;
};
int*?begin(X& x)?{return?reinterpret_cast<int*>(&x);
}
int*?end(X& x)?{return?reinterpret_cast<int*>(&x) +?sizeof(x) /?sizeof(int);
}
int?main()?{X x;for?(int?i : x) {std::cout << i << std::endl;}std::cout <<?"finish"?<< std::endl;return?0;
}
2.17/類類型的非類型模板參數
編譯器版本:GCC 9
C++20 允許使用類類型作為非類型模板參數,前提是該類的所有操作可以在編譯期完成,并且滿足特定條件(如支持常量比較等)。
基本用法示例:
#include?<iostream>
struct?A?{int?value;// operator== 必須是 constexprconstexpr?bool?operator==(const?A& v)?const?{return?value == v.value;}
};
template<A a, A b>
struct?Equal?{static?constexpr?bool?value = a == b;?// 編譯期比較
};
int?main()?{static?constexpr?A a{10}, b{20}, c{10};std::cout << std::boolalpha;std::cout << Equal<a, b>::value << std::endl;?// 輸出 falsestd::cout << Equal<a, a>::value << std::endl;?// 輸出 truereturn?0;
}
① operator== 的缺口與地址一致性
當使用類類型作為非類型模板參數時,如果兩個對象邏輯上相等(即?<=>?比較結果為 0),那么它們實例化的模板也應共享相同的地址。
#include?<iostream>
template<auto?v>
int?Value;
struct?A?{int?value;
};
int?main()?{static?constexpr?A a{10}, b{20}, c{10};std::cout << std::boolalpha;std::cout << (&Value<a> == &Value<b>) << std::endl;?// falsestd::cout << (&Value<a> == &Value<c>) << std::endl;?// truereturn?0;
}
② 成員函數調用要求 constexpr
由于模板參數必須在編譯期求值,因此類中的成員函數用于模板參數計算時,必須標記為?constexpr。
③ 類模板參數的相互推導
C++20 支持從字符串字面量等自動推導模板參數。
#include?<string>
template<typename?_Tp, std::size_t N>
struct?MyArray {constexpr MyArray(const?_Tp (&foo)[N +?1]) {std::copy_n(foo, N +?1, m_data);}auto operator<=>(const?MyArray&,?const?MyArray&) =?default;_Tp m_data[N];
};
template<typename?_Tp, std::size_t N>
MyArray(const?_Tp (&str)[N]) -> MyArray<_Tp, N -?1>;
template<std::size_t N>
using CharArray = MyArray<char, N>;
// 舊寫法需要顯式指定大小
template<std::size_t N, CharArray<N> Str>
struct?A {};
using hello_A = A<5,?"hello">;
// 新寫法可自動推導
template<CharArray Str>
struct?B {};
using hello_B = B<"hello">;
④ 用戶自定義字面量支持
結合上述特性,可以實現基于字符串字面量的模板參數化處理:
template<CharArray Str>
auto operator""_udl();
"hello"_udl; // 等價于 operator""_udl<"hello">()
類類型非類型模板參數的條件(滿足任意一個即可):
- 是字面量類型;
- 是左值引用;
- 包含占位符類型;
- 是派生類類型的占位符;
- 擁有強結構可比較性(即支持默認的?operator<=>,沒有 mutable 或 volatile 成員);
強結構可比較性的定義:對于任意類型 T,若?const T?的 glvalue 對象 x,使得?x <=> x?返回?std::strong_ordering?或?std::strong_equality,并且不調用用戶定義的三向比較運算符或結構化比較函數,則該類型具有強結構可比較性。
2.18、禁止使用用戶聲明的構造函數進行聚合初始化
編譯器版本:GCC 9
在 C++20 中,禁止使用用戶顯式聲明的構造函數(即使為?default?或?delete)來進行聚合初始化,從而修復了一些邊緣情況下的語義不一致問題。
舊版存在的幾個問題:
① delete 構造函數仍可聚合初始化
struct?X {X() = delete;
};
int?main()?{X x1; ? ? ??// 錯誤:X() 被刪除X x2{}; ? ??// 舊版本可能通過編譯(錯誤)
}
② 雙重聚合初始化
struct?X {int?i{4};X() =?default;
};
int?main()?{X?x1(3); ??// 錯誤:沒有匹配的構造函數X x2{3}; ??// 舊版本可能通過編譯(錯誤)
}
③ 類外 default 構造函數導致聚合失敗
struct?X?{int?i;X() =?default;
};
struct?Y?{int?i;Y();
};
Y::Y() =?default;
int?main()?{X x{4}; ??// 舊版本可能通過編譯(錯誤)Y y{4}; ??// 舊版本可能報錯(不一致)
}
C++20 的修正方案:
如果類中顯式聲明了除拷貝/移動構造函數以外的其他構造函數,則該類不能使用聚合初始化。
修正后的行為如下:
struct?X?{X() =?delete;
};
int?main()?{X x1; ? ? ?// 錯誤:構造函數被刪除X x2{}; ? ?// 錯誤:構造函數被刪除
}
struct?X?{int?i{4};X() =?default;
};
int?main()?{X?x1(3); ??// 錯誤:無匹配構造函數X x2{3}; ??// 錯誤:不允許聚合初始化
}
#include?<initializer_list>
struct?X?{int?i;X() =?default;
};
struct?Y?{int?i;Y();
};
Y::Y() =?default;
struct?A?{int?i;A(int);
};
struct?B?{int?i;B(int);
};
B::B(int) {}
struct?C?{int?i;C() =?default;C(std::initializer_list<int> list);
};
int?main()?{X x{4}; ? ?// 錯誤:無匹配構造函數Y y{4}; ? ?// 錯誤:無匹配構造函數A a{5}; ? ?// 正確B b{5}; ? ?// 正確C c{6}; ? ?// 正確:支持 initializer_list 構造函數return?0;
}
2.19、嵌套內聯命名空間
編譯器版本:GCC 9
C++20 引入了嵌套內聯命名空間的新語法,使得在定義多個層級的命名空間時更加簡潔,并且可以更靈活地控制符號的可見性。
舊寫法(使用?inline關鍵字):
#include?<iostream>
namespace?A {inline?namespace?B {void?func()?{std::cout <<?"B::func()"?<< std::endl;}}?// namespace B
}?// namespace A
int?main()?{A::func();?// 輸出 B::func()return?0;
}
新特性寫法(支持?A::inline C寫法):
#include?<iostream>
namespace?A {namespace?B {void?func()?{std::cout <<?"B::func()"?<< std::endl;}}?// namespace B
}?// namespace A
namespace?A::inline?C {void?func()?{std::cout <<?"C::func()"?<< std::endl;}
}
int?main()?{A::func();?// 輸出 C::func()return?0;
}
- A::inline C 是 namespace A { inline namespace C { ... } } 的簡寫形式。
- 內聯命名空間中的函數/變量默認在父命名空間中可見,用于實現向后兼容或接口簡化。
2.20、約束聲明的另一種辦法
編譯器版本:GCC 10
C++20 支持將?auto?和?concept?結合使用,從而簡化模板約束的寫法,使代碼更具表現力和可讀性。
示例:
#include?<iostream>
struct?Compare?{// 使用 auto 替代模板類型,自動推導參數bool?operator()(const?auto& t1,?const?auto& t2)?const?{return?t1 < t2;}
};
template<typename?T>
concept?CanCompare =?requires(T t) {t * t; ?// 需要支持乘法運算符Compare().operator()(T(),?T());?// 需要支持 < 運算符
};
// 使用 concept + auto 的函數返回值和參數
CanCompare?auto?pow2(CanCompare?auto?x)?{CanCompare?auto?y = x * x;return?y;
}
struct?A?{int?value =?0;bool?operator<(const?A& a)?const?{return?value < a.value;}A?operator*(const?A& a)?const?{return?{.value = a.value *?this->value};}
};
int?main()?{A a;a.value =?100;A aa =?pow2(a);?// 推導 x 為 A 類型,滿足 CanCompare 約束std::cout << aa.value << std::endl;return?0;
}
2.21、允許在常量表達式中使用 dynamic_cast、多態 typeid
編譯器版本:GCC 10
C++20 允許在?constexpr?上下文中使用?dynamic_cast?和?typeid,前提是它們的操作對象是已知的靜態類型或具有多態性的對象。
示例:
#include?<iostream>
#include?<typeinfo>
struct?Base?{virtual?~Base() =?default;
};
struct?Derived?: Base {};
constexpr?bool?test_dynamic_cast()?{Derived d;Base* b = &d;return?dynamic_cast<Derived*>(b) !=?nullptr;
}
static_assert(test_dynamic_cast(),?"dynamic_cast in constexpr failed");
constexpr?bool?test_typeid()?{Derived d;Base* b = &d;return?typeid(*b) ==?typeid(Derived);
}
static_assert(test_typeid(),?"typeid in constexpr failed");
2.22、允許用圓括號的值進行聚合初始化
編譯器版本:GCC 10
C++20 擴展了聚合初始化語法,允許使用圓括號?( )?來代替花括號?{ },只要目標類型是聚合類型。
示例:
#include <iostream>
struct?A {int?v;
};
struct?B {int?a;double?b;A&& c;long?long&& d;
};
A?get()?{return?A();
}
int?main()?{int?i =?100;B b1{1,?20.0, A(),?200}; ? ? ? ??// 正常聚合初始化B?b2(1,?20.0, A(), 300); ? ? ? ??// C++20 新增支持B b3{1,?20.0,?get(),?300}; ? ? ??// 正常B?b4(2,?30.0, std::move(get()), std::move(i));?// 正常return?0;
}
2.23、new 表達式的數組元素個數推導
編譯器版本:GCC 11
C++20 支持在使用?new?創建數組時省略大小,由編譯器根據初始化列表自動推導。
示例:
#include?<iostream>
#include?<cstring>
int?main()?{double?a[]{1,?2,?3}; ? ? ? ? ? ? ?// 普通數組推導double* p =?new?double[]{1,?2,?3};?// 自動推導大小為 3p =?new?double[0]{}; ? ? ? ? ? ? ?// 顯式指定大小為 0p =?new?double[]{}; ? ? ? ? ? ? ??// 自動推導大小為 0char* d =?new?char[]{"Hello"}; ??// 推導為包含 '\0' 的字符串數組int?size = std::strlen(d);std::cout << size << std::endl; ??// 輸出 5return?0;
}
2.24、Unicode 字符串字面量
編譯器版本:GCC 10
C++20 支持 UTF-16 和 UTF-32 編碼的字符串字面量,分別使用前綴?u?和?U。
示例:
#include <string>
int?main() {std::u16string str1 =?u"aaaaaa"; // UTF-16?字符串std::u32string str2 =?U"bbbbbb"; // UTF-32?字符串return?0;
}
2.25、允許轉換成未知邊界的數組
編譯器版本:GCC 10
C++20 允許將數組作為實參傳遞給接受未知邊界數組的形參。這種特性對于泛型編程非常有用。
示例:
template<typename?T>
static?void?func(T (&arr)[])?{// 接收任意大小的數組
}
template<typename?T>
static?void?func(T (&&arr)[])?{// 接收臨時數組
}
int?main()?{int?a[3];int?b[6];func<int>(a); ? ? ? ? ? ? ? ?// OKfunc<int>(b); ? ? ? ? ? ? ? ?// OKfunc<int>({1,?2,?3,?4}); ? ??// OKfunc<double>({1.0,?2,?3,?4,?8.0});?// OKreturn?0;
}
2.26、聚合初始化推導類模板參數
編譯器版本:GCC 8
C++20 支持通過聚合初始化的方式推導類模板參數類型,提升了模板編程的靈活性。
示例:
template?<typename?T>
struct?S?{T x;T y;
};
template?<typename?T>
struct?C?{S<T> s;T t;
};
template?<typename?T>
struct?D?{S<int> s;T t;
};
C c1 = {1,?2}; ? ? ? ? ? ??// error: deduction failed
C c2 = {1,?2,?3}; ? ? ? ? ?// error: deduction failed
C c3 = {{1u,?2u},?3}; ? ? ?// OK: 推導為 C<int>
D d1 = {1,?2}; ? ? ? ? ? ??// error: deduction failed
D d2 = {1,?2,?3}; ? ? ? ? ?// OK: 推導為 D<int>
template?<typename?T>
struct?I?{using?type = T;
};
template?<typename?T>
struct?E?{typename?I<T>::type i;T t;
};
E e1 = {1,?2}; ? ? ? ? ? ??// OK: 推導為 E<int>
2.27、隱式地將返回的本地變量轉換為右值引用
編譯器版本:GCC 11
C++20 引入了一項優化:在某些情況下,函數中返回局部變量時會自動使用移動語義(move)而不是復制(copy),即使沒有顯式使用?std::move。
觸發條件:
以下兩種情況會觸發隱式 move:
- return 或 co_return 表達式中的 id-expression 是函數最內層塊或 lambda 主體中聲明的“隱式可移動實體”;
- throw 表達式中引用的是一個“隱式可移動實體”,并且該實體的作用域不超過其所在的 try 塊或構造函數初始化列表。
??隱式可移動實體定義:
是局部變量;
沒有被 const 修飾;
不是數組;
不是通過花括號?{}?初始化的聚合類型;
沒有綁定到引用;
沒有被取地址(&)操作符使用過。
示例:
#include?<iostream>
struct?base?{base() {}base(const?base&) {std::cout <<?"base(const base &)"?<< std::endl;}
private:base(base&&)?noexcept?{std::cout <<?"base(base &&)"?<< std::endl;}
};
struct?derived?: base {};
base?f()?{base b;throw?b;?// 自動調用移動構造函數(如果可用)derived d;return?d;?// 自動調用移動構造函數(從 derived -> base)
}
int?main()?{try?{f();}?catch?(base) {// 輸出兩次 "base(base &&)"}return?0;
}
2.28、允許?default修飾按值比較的運算符
編譯器版本:GCC 10
C++20 支持對按值傳遞參數的比較運算符使用?= default,用于自動生成默認實現。
示例:
struct?C?{friend?bool?operator==(C, C) =?default;?// 合法!C++20 起支持
};
2.29、非類型模板參數等效的條件
編譯器版本:GCC 10
當兩個非類型模板參數的值被認為是“等效”的時候,它們可以被視為相同的模板實參。這在模板特化、別名推導等場景中非常重要。
等效條件(滿足任意一條即可):
類型 | 判斷條件 |
整型 | 值相同 |
浮點型 | 值相同 |
std::nullptr_t | 都為 nullptr |
枚舉類型 | 枚舉值相同 |
指針類型 | 指向同一對象或函數 |
成員指針類型 | 指向同一個類的同一成員,或者都為空 |
引用類型 | 引用同一個對象或函數 |
數組類型 | 所有元素都滿足等效條件 |
共用體類型 | 沒有活動成員,或具有相同的活動成員且其值等效 |
類類型 | 所有直接子對象和引用成員滿足等效條件 |
2.30、Destroying Operator Delete
編譯器版本:GCC 9
C++20 引入了新的?operator delete?形式:destroying operator delete,它允許在析構對象的同時控制內存釋放行為。
語法格式:
void?operator?delete(T* ptr, std::destroying_delete_t);
特點:
- 優先級高于普通?operator delete;
- 不會自動釋放內存
- ,只負責銷毀對象;
- 支持虛析構函數的行為一致性
- (即遵循多態規則);
- 僅適用于非數組類型的?delete?操作。
示例 1:基本用法
#include?<iostream>
#include?<new>?// 包含 std::destroying_delete_t
struct?A?{void?operator?delete(void* ptr)?{std::cout <<?"111"?<< std::endl;}void?operator?delete(A* ptr, std::destroying_delete_t)?{std::cout <<?"222"?<< std::endl;}
};
struct?B?{int?value =?10;void?operator?delete(B* ptr, std::destroying_delete_t)?{std::cout <<?"333"?<< std::endl;}
};
struct?C?{void?operator?delete(void* ptr)?{std::cout <<?"444"?<< std::endl;}
};
int?main()?{A* a =?new?A;delete?a;?// 輸出 222B* b =?new?B;b->value =?100;delete?b;?// 輸出 333,b->value 仍可訪問(未釋放內存)std::cout << b->value << std::endl;?// 輸出 100C* c =?new?C;delete?c;?// 輸出 444return?0;
}
示例 2:繼承與虛析構函數
#include?<iostream>
#include?<new>
struct?A?{virtual?~A() {}void?operator?delete(A* ptr, std::destroying_delete_t)?{std::cout <<?"111"?<< std::endl;}
};
struct?B?{virtual?~B() {}
};
struct?C?: A {void?operator?delete(C* ptr, std::destroying_delete_t)?{std::cout <<?"222"?<< std::endl;}
};
struct?D?: B {void?operator?delete(D* ptr, std::destroying_delete_t)?{std::cout <<?"333"?<< std::endl;}
};
int?main()?{A* a =?new?A;delete?a;?// 輸出 111C* c =?new?C;delete?c;?// 輸出 222B* b =?new?D;delete?b;?// 輸出 333(靜態類型為 B,但實際調用 D 的 destroying delete)return?0;
}
3、宏
3.1、__VA_OPT__宏
編譯器版本:GCC 12
在 C++20 中,__VA_OPT__?是一個用于支持?可變參數宏(variadic macros)中空變參處理?的新特性。它允許你在宏定義中根據是否存在可變參數來選擇性地插入內容。
示例 :根據不同參數執行不同邏輯
可以結合?__VA_OPT__?來根據是否傳入參數做不同的事情。
#define?CALL(func,?...)?func(__VA_OPT__(__VA_ARGS__))
調用示例:
CALL(foo); ? ? ? ? ? ? ?// 展開為 foo()
CALL(bar,?1,?2,?3); ? ? // 展開為 bar(1,?2,?3)
4、屬性
4.1、likely與?unlikely
編譯器版本:GCC 9
這兩個屬性用于向編譯器提示某個分支的執行概率,幫助其進行更高效的指令調度和分支預測優化。
適用于?if、switch?等控制流語句中的分支判斷,尤其在性能敏感的邏輯路徑中非常有用。
示例:
int?f(int?i)?{switch(i) {case?1: [[fallthrough]];?// 顯式說明允許 fall-through[[likely]]?case?2:?return?1;?// 高概率進入該分支[[unlikely]]?case?3:?return?2;?// 極低概率進入該分支}return?4;
}
4.2、no_unique_address
編譯器版本:GCC 9
此屬性用于指示某個非靜態成員變量可以不占用唯一地址空間,通常用于優化空類型(empty type)成員對象的內存布局。
示例 1:基本用法
#include?<iostream>
struct?A?{};?// 空類型
struct?C?{};
struct?B?{long?long?v;[[no_unique_address]] C a, b;
};
int?main()?{B b;std::cout << &b.v << std::endl;?// 輸出 v 的地址std::cout << &b.a << std::endl;?// 地址為 &v + 1std::cout << &b.b << std::endl;?// 地址為 &v + 2std::cout <<?sizeof(B) << std::endl;?// 輸出 8return?0;
}
示例 2:多個空對象共享地址空間
#include?<iostream>
struct?A?{};?// 空對象
struct?B?{int?v;[[no_unique_address]] A a, b, c, d, e, f, g;
};
int?main()?{B b;std::cout << &b.v << std::endl;std::cout << &b.a << std::endl;std::cout << &b.b << std::endl;std::cout << &b.c << std::endl;std::cout << &b.d << std::endl;std::cout << &b.e << std::endl;std::cout << &b.f << std::endl;std::cout << &b.g << std::endl;std::cout <<?sizeof(B) << std::endl;?// 輸出 8return?0;
}
4.3、nodiscard(帶消息支持)
編譯器版本:GCC 10
C++20 擴展了?[[nodiscard]]?屬性,允許為其附加一條自定義警告信息,提醒調用者不要忽略返回值。
示例:
[[nodiscard("返回值不可忽略,請檢查錯誤碼")]]
const?char*?get() {return?"";
}
int?main()?{get();?// 警告:ignoring return value of ‘const char* get()’, declared with attribute nodiscard: "返回值不可忽略,請檢查錯誤碼"return?0;
}
5、棄用
C++20 對一些長期存在但容易引發誤解或錯誤使用的語言特性進行了棄用(deprecation)處理。這些特性的使用仍然合法,但鼓勵開發者采用更安全、更明確的新方式替代它們。
5.1、Lambda 表達式中?[=]隱式捕獲?this
棄用說明:
在 lambda 表達式中使用?[=]?捕獲列表時,會隱式地將?this?指針按值捕獲,從而允許訪問類成員變量。但由于這種行為不直觀,容易導致懸空引用或難以察覺的生命周期問題,因此 C++20 中將其標記為棄用。
5.2、比較運算符的改進(棄用部分隱式轉換)
① 枚舉類型的隱式算術轉換被棄用
在 C++20 之前,枚舉類型可以隱式轉換為整型進行比較或算術運算。但在 C++20 中,這類操作已被標記為棄用。
舊寫法(棄用):
enum?E1 { e };
enum?E2 { f };
int?main()?{bool?b = e <=?3.7; ? ??// 棄用:e 被隱式轉換為 intint?k = f - e; ? ? ? ??// 棄用:f 和 e 被隱式轉換為 intauto cmp = e <=> f; ? ?// 錯誤:無法使用 spaceship 運算符return?0;
}
推薦做法:
- 如需進行數值比較,應顯式轉換為整型:
bool?b = static_cast<int>(e) <=?3.7;
int?k = static_cast<int>(f) - static_cast<int>(e);
- 若需要支持?<=>?比較,應為枚舉定義合適的運算符重載。
② 數組之間的比較被棄用
數組之間的直接比較(如?==、!=)在 C++20 中也被標記為棄用,因為其實際比較的是數組首地址,而非數組內容,這容易引起誤解。
舊寫法(棄用):
int?arr1[5];
int?arr2[5];
bool?same = arr1 == arr2; ??// 棄用:比較的是 &arr1[0] == &arr2[0]
auto?cmp = arr1 <=> arr2; ??// 錯誤:不支持 spaceship 運算符
推薦做法:
使用標準庫函數逐個比較數組內容:
#include <algorithm>
bool same = std::equal(std::begin(arr1), std::end(arr1), std::begin(arr2));
5.3、下標表達式中的逗號操作符被棄用
棄用說明:
在下標表達式中使用逗號操作符(,)來分隔多個表達式的行為,在 C++20 中被標記為棄用。雖然逗號操作符本身并未被棄用,但在數組索引上下文中使用它容易引起混淆。
舊寫法(棄用):
int?main()?{int?a[3] = {0,?1,?3};int?tmp1 = a[4,?1]; ? ? ??// tmp1 = a[1] = 1 (只取最后一個表達式)int?tmp2 = a[10,?1,?2]; ??// tmp2 = a[2] = 3 (同樣只取最后一個)return?0;
}
推薦做法:
將逗號操作符從下標表達式中移除,改為單獨計算索引值:
int?index?= (10,?1,?2);?//?明確寫出逗號表達式的結果
int?tmp2 = a[index]; ? ?//?明確表示索引
或者直接避免使用逗號表達式:
int?tmp1 = a[1];
int?tmp2 = a[2];
C++20 在保持語言強大性能的同時,引入了許多實用的新特性和語法改進,提升了代碼的可讀性、安全性和開發效率。從屬性增強到宏優化,從 lambda 捕獲規范到棄用易錯用法,這些變化體現了 C++ 標準不斷演進的方向。
掌握這些新特性,有助于我們編寫更現代、更可靠的 C++ 程序,并為未來學習更高版本打下堅實基礎。
點擊下方關注【Linux教程】,獲取編程學習路線、原創項目教程、簡歷模板、面試題庫、AI 知識庫、編程交流圈子。