C++20新特性个人总结
C++20新特性
目录
1.8 co_await、co_yield、co_return
2.21 允许在常量表达式中使用dynamic_cast多态typeid
2.30 destroying operator delete
C++20
编译器版本:GCC 11
__cplusplus:202002L
编译选项:-std=c++20
1 关键字
1.1 concept
编译器版本:GCC 10
concept乃重头戏之一,用于模板库的开发。功能类似于C#的泛型约束,但是比C#泛型约束更为强大。
concept用于声明具有特定约束条件的模板类型。
例子:数值类型约束
#include <type_traits>
// 声明一个数值类型的concept
template<typename T>
concept number = std::is_arithmetic<T>::value; // 对模板类型T添加std::is_arithmetic<T>::value为true的约束,并对具有约束的新的类型声明number
// 使用具有约束的类型,调用该函数时,T类型必须符合std::is_arithmetic<T>::value等于true,否则编译报错
template<number T>
void func(T t)
{ }
// 调用
func<int>(10); // 正确,std::is_arithmetic<int>::value为true
func<double>(20.0); // 正确,std::is_arithmetic<double>::value为true
struct A
{ };
func<A>(A()); // 错误,std::is_arithmetic<A>::value为false
1.2 requires
编译器版本:GCC 10
单纯一个concept还不够强大,真正让concept起飞的是这个requires,concept结合requires之后,对模板类型参数的约束可以细致到类型成员变量、类型成员函数甚至其返回值等等。
例子:约束类型具有指定名称的成员变量、成员函数
#include <type_traits>
template<typename T>
concept can_run = requires(T t)
{
std::is_class<T>::value; // T是一个类型
t(); // T类型有重载括号运算符,且是无参的
t.run(); // T类型具有run()成员函数
std::is_same<decltype(t.run()), int>::value; // T类型的run()函数的返回值为int类型
}
// concepts类型使用
template<can_run T>
int func(T t)
{
t();
return t.run(); // run()函数的返回值已被限定为int类型,所以此处可直接返回
}
func<int>(10); // 错误,这不是一个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
template<class T> T::R f(); // OK, return type of a function declaration at global scope
// 作为函数的参数,
template<class T> void f(T::R); // Ill-formed (no diagnostic required), attempt to declare a void variable template
template<typename T>
struct PtrTraits
{
typedef T* Ptr;
};
template<class T>
struct S
{
using Ptr = PtrTraits<T>::Ptr; // OK, in a defining-type-id
T::R f(T::P p)
{ // OK, class scope
return static_cast<T::R>(p); // OK, type-id of a static_cast
}
auto g() -> S<T*>::Ptr;// OK, trailing-return-type
};
template<typename T> void f()
{
void (*pf)(T::X); // Variable pf of type void* initialized with T::X
void g(T::X); // Error: T::X at block scope does not denote a type
// (attempt to declare a void variable)
}
1.4 explicit
编译器版本:GCC 9
新增版本:C++11,可查看C++11新特性进行回顾
新增bool参数,表示explicit本身的作用是否启用
例子:
struct A
{
explicit(false)
A(int) { }
};
struct B
{
explicit(true)
B(int) { }
};
A a = 10; // 正确
B b = 10; // 错误:将int类型转换为B类型
1.5 constexpr
编译器版本:GCC 9
新增版本:C++11,可查看C++11新特性进行回顾
①扩展适用范围,新增对虚函数的支持,用法与普通函数一致,不再赘述。
②禁止constexpr函数内使用try-catch语句块。不再赘述。
1.6 char8_t
编译器版本:GCC 9
为utf-8字符编码专门打造,以后就由char8_t类型接收utf-8字面量,而不再由char接收。
编译器未完全实现,待续。
1.7 consteval
编译器版本:GCC 11
用于标识一个函数是“立即函数”,该函数将在编译期完成运算,其返回值也将是在编译期得到,其参数也必须能在编译期计算得到,而且函数内部的计算也都必须是能够在编译期运算出结果的。
比constexpr更为严格,严格限制在编译期范围。constexpr标识的函数中,会自动根据参数进行调整,如果参数是编译期常量,那调用的结果将在编译期计算完成;而如果参数是运行期变量,那函数调用将会转变为运行期计算。
例子:
#include <iostream>
constexpr int f(int a)
{
return a * a;
}
// 参数a必须是编译期常量
consteval int func(int a)
{
return f(a); // ok,因为f()可以在编译期运算
}
int main()
{
int a;
std::cin >> a;
int r1 = f(a); // ok,a是运行期变量,此次f()调用变成运行期计算
int r2 = func(a); // error,因为a是运行期变量
int r3 = func(1000); // ok
int r4 = func(f(10)); // ok,因为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]
result即协程最终结果值,不传时为void。
co_yield value
value即协程挂起时返回的值,不可省略,必须与co_return同类型,当co_return void时,不可使用co_yield,每次执行到co_yield都可以获取一次协程的值。
co_await value
①co_await运算符可重载,重载的co_await运算符函数参数类型为value的类型,运算符函数须返回一个awaiter。
②如果value就是调用一个函数,函数须返回一个awaiter。
所有拥有这三个关键字其中一个以上的函数,都将会被转换成协程函数。
1.8.2 awaiter说明
awaiter类型必须实现的三个接口
await_ready
co_await接收到参数时首先执行的函数,返回值bool。当返回值为false时,协程将被挂起,然后执行await_suspend函数;当返回值为true时,协程继续,将跳过await_suspend函数。
await_suspend
await_suspend返回类型可以为void,也可为bool。当返回值为bool时,如果返回false,则恢复协程。如果返回值为void,则等效于返回true。
关于await_suspend的实参——协程的句柄,可以是默认的std::coroutine_handle<>,也可以是指定的std::coroutine_handle<promise_type>。如果实参类型是指定的协程句柄,则是co_await所在协程函数的协程句柄。
promise_type,即协程挂起的对象,std::coroutine_handle是标准库提供的,用于存放promise_type引用,可控制协程的唤醒执行操作。
await_resume
await_resume的返回值就是co_await运算符的返回值。当协程在co_await挂起后再次被恢复时,或者协程在await_ready返回true时,将会调用await_resume。
1.8.3 协程函数
协程函数的返回值类型generator必须拥有名为promise_type的内部类型,这个返回值不需要用户自己写return,而是由编译器处理,调用promise_type的get_return_object()函数,获得协程函数的返回值——generator对象。
通常generator需要存放协程句柄,用于用户对协程的控制。
1.8.4 promise_type
promise_type提供的接口如下表(需要用到哪个就实现哪个):
函数描述 | 说明 |
generator get_return_object() | 在协程函数执行前调用。在该函数中,需要构造generator对象,然后将存放了promise_type引用的协程句柄传送到generator对象中存放。该接口的返回值就是协程函数的返回值。 |
awaiter initial_suspend() |
在协程函数初始化时调用,此处的awaiter同1.8.2,是否挂起由awaiter控制。 |
awaiter final_suspend() noexcept(true) | 在协程结束时调用。必须是noexcept。 |
awaiter yield_value(type value) | co_yield时调用,value即是co_yield的参数。 |
void unhandled_exception() | 协程函数发生异常时调用。 |
void return_void() | 当co_return且无返回值时调用。有该函数的时候不能存在return_value函数。 |
void return_value(type value) | 当co_return且有返回值时调用,value即是co_return的参数。有该函数的时候不能存在return_void函数。 |
static generator get_return_object_on_allocation_failure() |
当标识为noexcept的内存分配函数返回nullptr时,协程在返回generator时将会通过调用此接口获得返回值 |
1.8.5 综合使用案例
#include <iostream>
#include <thread>
#include <exception>
#include <coroutine>
#include <string>
#include <chrono>
#include <functional>
#include <future>
struct TimeAwaiter
{
std::chrono::system_clock::duration duration;
TimeAwaiter(std::chrono::system_clock::duration d) : duration(d) {}
bool await_ready() const
{
std::cout << __func__ << std::endl;
return duration.count() <= 0;
}
void await_resume()
{
std::cout << __func__ << std::endl;
}
void await_suspend(std::coroutine_handle<> h)
{
std::cout << __func__ << std::endl;
}
};
template<typename _Res>
struct FuncAwaiter
{
_Res value;
bool await_ready() const
{
std::cout << __func__ << std::endl;
return false;
}
_Res await_resume()
{
std::cout << __func__ << std::endl;
return value;
}
template<typename _Tp>
void await_suspend(std::coroutine_handle<_Tp> h)
{ std::cout << __func__ << std::endl; }
};
auto operator co_await(std::chrono::system_clock::duration d)
{
return TimeAwaiter{d};
}
template<
typename _Out,
typename _Task,
typename _InitAction = std::suspend_always,
typename _FinalAction = std::suspend_always,
typename _YieldAction = std::suspend_always>
struct PromiseImpl
{
using init_action_type = _InitAction;
using final_action_type = _FinalAction;
using yield_action_type = _YieldAction;
using task = _Task;
using self_type = PromiseImpl<_Out, _Task, _InitAction, _FinalAction, _YieldAction>;
_Out current_value;
static auto get_return_object_on_allocation_failure()
{
std::cout << __func__ << std::endl;
return task{nullptr};
}
task get_return_object()
{
std::cout << __func__ << std::endl;
return task{std::coroutine_handle<self_type>::from_promise(*this)};
}
init_action_type initial_suspend()
{
std::cout << __func__ << std::endl;
return init_action_type{};
}
final_action_type final_suspend() noexcept(true)
{
std::cout << __func__ << std::endl;
return final_action_type{};
}
void unhandled_exception()
{
std::cout << __func__ << std::endl;
std::terminate();
}
void return_value(const _Out &value)
{
std::cout << __func__ << std::endl;
current_value = value;
}
yield_action_type yield_value(const _Out &value)
{
std::cout << __func__ << std::endl;
current_value = value;
return yield_action_type{};
}
};
template<
typename _Task,
typename _InitAction,
typename _FinalAction,
typename _YieldAction>
struct PromiseImpl<void, _Task, _InitAction, _FinalAction, _YieldAction>
{
using init_action_type = _InitAction;
using final_action_type = _FinalAction;
using yield_action_type = _YieldAction;
using task = _Task;
using self_type = PromiseImpl<void, _Task, _InitAction, _FinalAction, _YieldAction>;
static auto get_return_object_on_allocation_failure()
{
std::cout << __func__ << std::endl;
return task{nullptr};
}
task get_return_object()
{
std::cout << __func__ << std::endl;
return task{std::coroutine_handle<self_type>::from_promise(*this)};
}
init_action_type initial_suspend()
{
std::cout << __func__ << std::endl;
return init_action_type{};
}
final_action_type final_suspend() noexcept(true)
{
std::cout << __func__ << std::endl;
return final_action_type{};
}
void unhandled_exception()
{
std::cout << __func__ << std::endl;
std::terminate();
}
void return_void()
{
std::cout << __func__ << std::endl;
}
yield_action_type yield_value(void)
{
std::cout << __func__ << std::endl;
return yield_action_type{};
}
};
template<
typename _Value,
typename _InitAction = std::suspend_always,
typename _FinalAction = std::suspend_always,
typename _YieldAction = std::suspend_always>
struct Task
{
public:
using value_type = _Value;
using reference = typename std::add_lvalue_reference<_Value>::type;
using const_reference = typename std::add_const<typename std::add_lvalue_reference<_Value>::type>::type;
using rvalue_reference = typename std::add_rvalue_reference<_Value>::type;
using init_action_type = _InitAction;
using final_action_type = _FinalAction;
using yield_action_type = _YieldAction;
using self = Task<value_type, init_action_type, final_action_type, yield_action_type>;
using promise_type = PromiseImpl<value_type, self, init_action_type, final_action_type, yield_action_type>;
using promise_handle = std::coroutine_handle<promise_type>;
friend class PromiseImpl<value_type, self, init_action_type, final_action_type, yield_action_type>;
bool next() { return coro ? (coro.resume(), !coro.done()) : false; }
const_reference value()
{
if constexpr (!std::is_same<void, _Value>::value)
{ return coro.promise().current_value; }
}
Task(Task const&) = delete;
Task(Task && rhs) : coro(rhs.coro) { rhs.coro = nullptr; }
~Task() { if (coro) coro.destroy(); }
private:
Task(promise_handle h) : coro(h) {}
promise_handle coro;
};
using generator_with_arg = Task<int>;
using generator_without_arg = Task<void>;
using namespace std::chrono;
static std::string test_await_print_func(const std::string &arg)
{
std::this_thread::sleep_for(1000ms);
std::cout << __func__ << std::endl;
return std::string("emmmmmmm ") + arg + " >>>> " + __func__;
}
template<typename _Callable, typename ... _Args>
static auto await_for(_Callable&& f, _Args&&... args) -> FuncAwaiter<decltype(f(args...))>
{
using result_type = decltype(f(args...));
// std::future<result_type> ret = std::async(std::launch::async, std::move(f), std::forward<_Args>(args)...);
// return FuncAwaiter<result_type>{ret};
return FuncAwaiter<result_type>{f(std::forward<_Args>(args)...)};
}
static generator_with_arg f1()
{
std::cout << "11111111111" << std::endl;
co_yield 1;
std::cout << "22222222222" << std::endl;
co_yield 2;
std::cout << "3333333333" << std::endl;
co_return 3;
}
static generator_without_arg f2()
{
std::cout << "11111111111" << std::endl;
std::cout << "22222222222" << std::endl;
std::cout << "3333333333" << 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 await_for(test_await_print_func, "yohohoho");
std::cout << ret << std::endl;
std::cout << "resumed 22222\n";
}
int main()
{
auto g1 = f1();
std::cout << "4444444444" << std::endl;
while (g1.next()) std::cout << "value: " << g1.value() << std::endl;
std::cout << "555555555" << std::endl;
std::cout << "==========================" << std::endl;
auto g2 = f2();
std::cout << "6666666666" << std::endl;
while (g2.next()) std::cout << "value: void" << std::endl;
std::cout << "777777777" << std::endl;
std::cout << "==========================" << std::endl;
auto g3 = test_co_await();
std::cout << "88888888888" << std::endl;
while (g3.next()) std::cout << "value: void" << std::endl;
std::cout << "99999999999999" << std::endl;
return 0;
}
输出结果:
1.9 constinit
编译器版本:GCC 10
用于强制常量进行初始化,不可动态初始化。
变量条件:静态 或 线程存储持续时间。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(); // 编译正确
constinit const char *str2 = get_str1(); // 编译错误,用非constexpr函数对constinit变量进行初始化
constinit const char *str3 = hahah; // 编译错误,用非常量表达式对constinit变量进行初始化
int main()
{
static constinit const char *str4 = get_str2(); // 编译正确
constinit const char *str5 = get_str2();// 编译错误,必须是静态 或 线程存储持续时间的变量
constinit thread_local const char *str6; // 编译正确
return 0;
}
2 语法
2.1 位域变量的默认成员初始化
编译器版本:GCC 8
位域变量在声明时可进行初始化。
位域变量的声明语法格式:
- 标识符 变量名 : 位数
- 标识符 变量名 : 常量表达式、大括号
例子:
int a;
const int b = 1;
struct S
{
int x1 : 8 = 42; // 正确,x1为8位的变量,并且初始化为42,“=42”为常量表达式
int x2 : 6 {42}; // 正确,x2为6位的变量,并且初始化为42
int x3 : true ? 10 : a = 20; // 正确,x3为10位变量,不进行初始化,赋值号优先于三目运算符
int x4 : true ? 10 : b = 20; // 错误,b为const变量,不可赋值
int x5 : (true ? 10 : b) = 20; // 正确,x5为10位的变量,并且初始化为20
int x6 : false ? 10 : a = 20; // 错误,a = 10不是常量表达式
};
2.2 修改const限定的成员指针
编译器版本:GCC 8
在一个右值的 .* 表达式中,如果表达式的第二个参数是指向以&修饰的成员函数的指针,那么这个程序就是不规范的,除非限定符是const
例子:
struct S { void foo() const& { } };
void f()
{
S{}.foo(); // 正确,没问题
(S{}.*&S::foo)(); // C++20起支持该语法
}
2.3 允许lambda表达值按值捕获this
编译器版本:GCC 8
例子:
struct S
{
int value;
void print()
{
auto f = [=, this]() {
this->value++;
};
}
}
2.4 指定初始化
编译器版本:GCC 8
在构造对象时,可以指定成员进行初始化,但是初始化的顺序必须与成员的内存顺序一致。
例子:
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); // 正确,调用#1
f({.y = 1, .x = 2}, 0); // 错误,调用#1,初始化顺序不匹配
f({.y = 1, .x = 2}, 1, 2, 3); // 正确,调用#2
g({.x = 1, .y = 2}); // 错误,无法确定调用#3还是#4
}
2.5 lambda表达式支持模板
编译器版本:GCC 8
从新版开始,lambda表达式支持模板编程,且支持自动推导。(官方的说明是:支持未鉴定的上下文)
例子1:
int a;
auto f = [&a]<typename T>(const T &m) {
a += m;
};
f(10);
例子2:
template<typename T>
int func(int t)
{
return t * t;
}
int f()
{
return func<decltype([] {})>(20);
}
例子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
声明变量时进行初始化,如果能从构造函数中推导出变量类型,则该变量的类型可以不用指定模板参数。
例子:
vector v{vector{1, 2}}; // 正确,v 推导为vector<vector<int>>类型
tuple t{tuple{1, 2}}; //正确,t 推导为tuple<int, int>类型
2.7 简化lambda的隐式捕获
编译器版本:GCC 8
本人水平有限,暂时不能展示。这里有个提案的文档:P0588R1: Simplifying implicit lambda capture
2.8 ADL与不可见的模板函数
编译器版本:GCC 9
ADL是C++本来就有的机制,用于自动推断调用的函数的位置,从而简化代码的编写。而新特性扩展了ADL机制,可以用于模板函数的推断。
例子:
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);
}
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<=>
编译器版本:GCC 10
因为篇幅过长就不再在这里详细赘述了,感兴趣的可以自行查看http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0515r3.pdf。因此,我在这里只简单说一下。
来了解一下不同类型的比较策略(专有名词就不翻译了),并且可进行向下进行相对应的隐式转换:
策略 | 数值型结果 | 非数值型结果 | ||
-1 | 0 | 1 | ||
strong_ordering | less | equal | greater | unordered |
weak_ordering | less | equivalent | greater | |
partial_ordering | less | equivalent | greater | |
strong_equality | unequal | equal | unequal | |
weak_equality | nonequivalent | equivalent | nonequivalent |
至于什么时候用到哪一种策略,这里有一位博主翻译好了的[翻译]C++20新运算符之三元比较符<=>_吾碎汝梦丶S1的博客-CSDN博客_三元比较运算符,这里不再讲解(懒)。
2.10 基于范围的for循环初始化
编译器版本:GCC 9
新增的for循环语法格式:
for([init-statement;] for-range-declaration : for-range-initializer) ...
例子:
int a[] = {1, 2, 3, 4};
for(int b = 0; int i : a)
{
...
}
2.11 默认可构造可分配的无状态lambdas
编译器版本:GCC 9
简单点说,就是可以获取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; // error: 'struct A::impl1' is private within this context
};
template<typename T>
struct trait
{
A::impl1 t; // ok
A::impl2<T> t2; // ok
void func()
{
A::impl1 tmp; // ok
tmp.value = 10;// ok
t2.value = 20; // ok
A::impl3 t3; // ok
t3.value = 30; // ok
}
};
int main()
{
trait<int> a;
a.t.value = 10; // ok
a.t2.value = 20; // error: 'int A::impl2<int>::value' is private within this context
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, B ? f<T>() : 0 is not potentially constant evaluated
h<false, int>(0); // error, instantiates f<int> even though B evaluates to false and
// list-initialization of int from int cannot be narrowing
}
2.14 允许lambda在初始化捕获时进行包扩展
编译器版本:GCC 9
扩展了包扩展的应用范围
例子:
#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
这个特性比较地牛逼了,以前的结构化绑定的限制比较多,现在放宽了限制,并且可以自定义绑定的第几个是哪个类型,而且可以指定解绑的个数。
自定义的条件:
①在类外实现get<int>(Type)函数、或在类内实现Type::get<int>()成员函数;
②在std命名空间内特化tuple_size和tuple_element结构体;
③get<int>()的返回路径数量必须与tuple_size指定的数值相等,tuple_element特化的索引数量(且必须从0开始)必须与tuple_size指定的数值相等;
④get<int N>()函数中N的值对应的返回类型必须与tuple_element对应索引指定的类型相同。
例子1:
#include <string>
#include <tuple>
struct A
{
int a;
int b;
};
struct X : private A
{
std::string value1;
std::string value2;
};
// 第一种方式,类外实现get<>()
template<int N>
auto& get(X &x)
{
if constexpr (N == 0)
return x.value2;
}
namespace std
{
// 指定结构化绑定数量为1个
template<>
class tuple_size<X>
: public std::integral_constant<int, 1>
{};
// 指定结构化绑定的第一种类型为string
template<>
class tuple_element<0, X>
{
public:
using type = std::string;
};
}
int main()
{
X x;
auto& [y] = x;// y的类型为string
auto& [y1, y2] = x; // error: 2 names provided for structured binding, while 'X' decomposes into 1 element
return 0;
}
例子2:
#include <string>
#include <tuple> // 必须包含tuple库
struct A
{
int a;
int b;
};
struct X : protected A
{
std::string value1;
std::string value2;
// 第二种方式,在类内实现get<>
template<int N>
auto& get()
{
if constexpr (N == 0)
return value1;
else if constexpr (N == 1)
return a;
}
};
namespace std
{
// 指定X类型结构化绑定的个数为2个
template<>
class tuple_size<X>
: public std::integral_constant<int, 2>
{};
// 指定第一种类型为string类型
template<>
class tuple_element<0, X>
{
public:
using type = std::string;
};
// 指定第二种类型为int类型
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
以前的版本自定义类的for循环,需要实现begin()和end()的成员函数;新版本开始,可以不实现成员函数,而在类体外实现begin()和end(),具体看以下例子
例子:
#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
比较拗口,放松了非类型模板参数的限制,可以用类类型作为模板的参数,但是条件是所需要的运算需要在编译期完成。
如下例:
#include <iostream>
struct A
{
int value;
// 这里的constexpr是必须的
constexpr bool operator==(const A &v) const
{ return value == v.value; }
};
template<A a, A b>
struct Equal
{
static constexpr bool value = a == b;// 需要在编译期调用operator==
};
int main()
{
static constexpr A a{10}, b{20}; // 作为模板的传入参数,也必须是常量
std::cout << std::boolalpha << Equal<a, b>::value << std::endl; // 输出false
std::cout << std::boolalpha << Equal<a, a>::value << std::endl; // 输出true
return 0;
}
关于类类型的非类型模板参数的优化
①operator==的缺口
直接看例子,文字不好写
#include <iostream>
template<auto v>
int Value;
struct A
{
int value;
};
int main()
{
static constexpr A a{10}, b{20}, c{10};
// 对于Value<a>和Value<b>,只要 (a<=>b) == 0,则&Value<a> == &Value<b>结果就true.
// 关于 <=> 运算符可以往上面看
std::cout << std::boolalpha << (&Value<a> == &Value<b>) << std::endl; // 输出false
std::cout << std::boolalpha << (&Value<a> == &Value<c>) << std::endl; // 输出true
return 0;
}
②模板参数的成员函数调用
因为模板参数是处于编译期计算的,因此,作为调用用于自定义类型的模板参数的成员函数时,这些成员必须是constexpr修饰的。
③类模板参数的相互推导
例子:
#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>;
// 在此例子中,用"hello"字符串去实例化A模板时,需要显式的提供size,这导致比较大的不便
template <std::size_t N, CharArray<N> Str>
struct A {};
using hello_A = A<5, "hello">;
// 既然这是编译期常量,那在编译期是可以计算出来的,因此C++20做了优化
template <CharArray Str>
struct B {};
using hello_B = B<"hello">;
④用户自定义字面量
引用上一个例子
template <CharArray Str>
auto operator"" _udl();
"hello"_udl; // 等价于operator""_udl<"hello">()
类类型的非类型模板参数的条件(满足任意一个):
①字面量
②是一个lvalue
③包含占位符的类型
④派生类类型的一个占位符
⑤拥有强结构可比较性,没有mutable或者volatile修饰的子对象,拥有声明为public且指定为default的operator<=>
关于强结构可比较性的定义:
对于任意一种类型T,const T的一个glvalue对象x,x<=>x是类型std::strong_ordering或者std::strong_equality的有效表达式,它既不调用三向比较操作符,也不调用结构比较运算符。
2.18 禁止使用用户自己声明的构造函数来进行聚合初始化
编译器版本:GCC 9
旧版的几个问题
①delete了构造函数,却依然可以实例化
struct X
{
X() = delete;
};
int main()
{
X x1; // 错误,无参构造函数为delete
X x2{}; // 编译通过了(问题一,实际上应该编译不通过才对)
return 0;
}
②双重聚合初始化
struct X
{
int i{4};
X() = default;
};
int main()
{
X x1(3); // 错误,没有带int类型的构造函数
X x2{3}; // 编译通过,(问题二,非静态数据成员的双重聚合初始化)
return 0;
}
③类外指定构造函数default
struct X
{
int i;
X() = default;
};
struct Y
{
int i;
Y();
};
Y::Y() = default;
int main()
{
X x{4}; // 正常,编译通过
Y y{4}; // 编译不通过(问题三,Y结构被判定为非聚合结构)
return 0;
}
解决方案
简化并统一初始化语义
如果用户显式声明了非移动和拷贝构造函数的其他构造函数,则类的对象必须通过其中一个构造函数进行初始化。
上面三个问题的修正结果:
struct X
{
X() = delete;
};
int main()
{
X x1; // 编译错误,无参构造函数为delete
X x2{}; // 编译错误,无参构造函数为delete
return 0;
}
struct X
{
int i{4};
X() = default;
};
int main()
{
X x1(3); // 错误,没有X::X(int)构造函数
X x2{3}; // 错误,没有X::X({...})构造函数
return 0;
}
#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}; // 编译错误,没有X::X({...})构造函数
Y y{4}; // 编译错误,没有X::X({...})构造函数
A a{5}; // 编译通过
B b{5}; // 编译通过
C c{6}; // 编译通过
return 0;
}
2.19 嵌套内联命名空间
编译器版本:GCC 9
简化内联命名空间的嵌套语法
旧例子:
#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;
}
新特性例子:
#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;
}
} // namespace C
int main()
{
A::func(); // 输出C::func()
return 0;
}
2.20 约束声明的另一种办法
编译器版本:GCC 10
利用concept与auto的特性,增加了新的约束声明方法。
例子:
#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; // T类型需要提供*运算符
Compare().operator()(T(), T()); // 根据Compare结果体,需要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类型,A类型符合CanCompare约束,编译通过
std::cout << aa.value << std::endl;
return 0;
}
2.21 允许在常量表达式中使用dynamic_cast多态typeid
编译器版本:GCC 10
待续
2.22 允许用圆括弧的值进行聚合初始化
编译器版本:GCC 10
简单地说,就是相当于默认有一个有全部非静态数据成员的构造函数。前提条件:目标类型必须符合聚合初始化的条件。
例子:
#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); // 编译通过
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}; // 编译通过
p = new double[0]{}; // 编译通过
p = new double[]{}; // 编译通过
char *d = new char[]{"Hello"}; // 编译通过
int size = std::strlen(d);
std::cout << size << std::endl; // 输出5
return 0;
}
2.24 unicode字符串字面量
编译器版本:GCC 10
新增两种字面量,分别是utf-16和utf-32编码字符串字面量
例子:
#include <string>
int main()
{
std::u16string str1 = u"aaaaaa"; // 小写u是utf-16字符串
std::u32string str2 = U"bbbbbb"; // 大写U是utf-32字符串
return 0;
}
2.25 允许转换成未知边界的数组
编译器版本:GCC 10
这个特性比较简单,在实参为数组的传参时形参可以是无边界的数组。
例子:
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);
func<int>(b);
func<int>({1, 2, 3, 4});
func<double>({1.0, 2, 3, 4, 8.0});
return 0;
}
乍一看,好像很鸡肋的特性,不知道数组的长度,长度无法获取,数组的遍历不知道终点,暂时不清楚应用场景。
2.26 聚合初始化推导类模板参数
编译器版本:GCC 8
通过聚合初始化中的参数类型 来 推导出类模板参数类型
例子:
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> deduced
D d1 = {1, 2}; // error: deduction failed
D d2 = {1, 2, 3}; // OK, braces elided, D<int> deduced
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> deduced
2.27 隐式地将返回的本地变量转换为右值引用
编译器版本:GCC 11
在以下的复制操作中,将会隐式采用移动操作代替复制操作的情况:
①如果return或co_return中的表达式是一个id-expression,其是在函数的最内层语句块或lambda表达式的主体或者参数声明子句中声明的隐式可移动实体。
②throw表达式的一个隐式可移动实体id-expression,其范围不超出最内层try块 或 [复合语句或构造函数初始值包含该throw表达式的函数try块(如果有)] 的复合语句。
例子:
#include <iostream>
struct base {
base() {}
base(const base &)
{ std::cout << "base(const base &)" << std::endl; }
private:
base(base &&)
{ std::cout << "base(base &&)" << std::endl; }
};
struct derived : base {};
base f() {
base b;
throw b; // move
derived d;
return d;
}
int main()
{
try
{
f();
}
catch(base)
{ }
return 0;
}
2.28 允许default修饰运算符按值比较
编译器版本:GCC 10
直接例子:
struct C
{
// 参数为按值传递
friend bool operator==(C, C) = default; // C++20起支持
};
2.29 非类型模板参数等效的条件
相同类型的两个值,模板参数等效的条件(之一):
①整型且值相同;
②浮点类型且值相同;
③是std::nullptr_t类型;
④枚举类型,且枚举值相同;
⑤指针类型,且指针值相同;
⑥指向成员的指针类型,且引用相同的类成员,或者都是空成员指针值;
⑦引用类型,且引用相同的对象或函数;
⑧数组类型,对应元素满足模板参数等效;
⑨共用体类型,或者都没有活动成员,或者都具有相同的活动成员,且活动成员都是满足模板参数等效;
⑩类类型,且对应的直接子对象和引用成员满足模板参数等效。
2.30 destroying operator delete
编译器版本:GCC 9
新增的delete运算符函数,必须是类型的成员函数,且数组的delete不适用。
有以下特点:
①如果存在destroying operator delete和普通的operator delete,会优先调用destroying operator delete。
②调用destroying operator delete并不会释放内存。
③如果析构函数是虚函数,destroying operator delete遵循虚函数规则(不需要声明virtual)。
语法格式:void operator delete(type *, std::destroying_delete_t);
其中type是具体的类型
例子1:
#include <iostream>
#include <new> // std::destroying_delete_t在new头文件
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;
B *b = new B;
b->value = 100;
delete b;
std::cout << b->value << std::endl; // 输出100,因为内存未被释放,所以此处b->value不会发生任何异常
C *c = new C;
delete c;
return 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;
C *c = new C;
delete c;
B *b = new D;
delete b;
return 0;
}
3 宏
3.1 __VA_OPT__
编译器版本:GCC 12
编译器未支持,待续。
4 属性
4.1 likely和unlikely
编译器版本:GCC 9
该属性用于指示switch分支结构的优化,likely表示“很大可能”落到指定分支,而unlikely表示“很小概率”落到指定分支。
例子:
int f(int i)
{
switch(i) {
case 1: [[fallthrough]];
[[likely]] case 2: return 1;
[[unlikely]] case 3: return 2;
}
return 4;
}
4.2 no_unique_address
编译器版本:GCC 9
这个属性比较的复杂,有以下特性:
①同类型的子对象或成员不占用同一个地址;
②当地址不够分配时,则按照一般做法扩展空间,继续为未分配地址的no_unique_address属性成员分配地址,直至全部分配完毕;
③该属性对空类型(没有非静态数据成员)有效。
例子1:
#include <iostream>
struct A
{ }; // 空类型
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; // a地址为 &v + 1
std::cout << &b.b << std::endl; // b地址为 &v + 2
std::cout << sizeof(B) << std::endl; // 输出 8
return 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; // 得到v地址
std::cout << &b.a << std::endl; // a地址为 &v + 1
std::cout << &b.b << std::endl; // a地址为 &v + 2
std::cout << &b.c << std::endl; // a地址为 &v + 3
std::cout << &b.d << std::endl; // a地址为 &v + 4
std::cout << &b.e << std::endl; // a地址为 &v + 5
std::cout << &b.g << std::endl; // a地址为 &v + 6
std::cout << &b.f << std::endl; // a地址为 &v + 7
// 由于空间不足,按照一般的内存对齐方式自动扩展空间
std::cout << sizeof(B) << std::endl; // 输出 8
return 0;
}
例子3:
#include <iostream>
struct A
{ [[no_unique_address]] int value; };
struct B
{
int v;
[[no_unique_address]] A a, b, c;
};
int main()
{
B b;
std::cout << &b.v << std::endl; // 得到v地址
std::cout << &b.a << std::endl; // a地址为 &v + 4
std::cout << &b.b << std::endl; // a地址为 &v + 8
std::cout << &b.c << std::endl; // a地址为 &v + 12
std::cout << sizeof(B) << std::endl;// 输出16
return 0;
}
4.3 nodiscard
编译器版本:GCC 10
新增可选信息
例子:
[[nodiscard("asdfasfa")]]
const char * get()
{
return "";
}
int main()
{
get(); // warning: ignoring return value of 'const char* get()', declared with attribute 'nodiscard': 'asdfasfa' [-Wunused-result]
return 0;
}
5 弃用
5.1 lambda弃用使用[=]来隐式捕获this
struct X
{
int x;
void foo(int n)
{
auto f = [=]() { x = n; }; // 弃用:此处的x是this->x,而非拷贝
auto g = [=, this]() { x = n; }; // 新版推荐的方法
}
};
5.2 比较运算符的改进
①弃用枚举的隐式算术转换
enum E1 { e };
enum E2 { f };
int main()
{
bool b = e <= 3.7; // deprecated
int k = f - e; // deprecated
auto cmp = e <=> f; // ill-formed
return 0;
}
②数组的比较
int arr1[5];
int arr2[5];
bool same = arr1 == arr2; // deprecated, 效果与&arr1[0] == &arr2[0]相同,并非比较数组内容
auto cmp = arr1 <=> arr2; // ill-formed
5.3 弃用下标表达式中的逗号操作符
在下标访问时,弃用逗号分隔的多个参数的语法。
如例子:
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;
}
后记
关于C++20新特性的英文文档本人已提供免费下载,感兴趣的可以自行下载:C++20新特性.pdf_c++20新特性-C++文档类资源-CSDN下载
我的内容只是展示应用层面,而不对新特性的目标进行阐述,因为这样子可以少写很多字。
另外,如有问题,欢迎指出。
更多推荐
所有评论(0)