抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

C++20特性

现在是2023年,C++23都出来很久了(尽管很多编译器没有实现全功能),但还是有着大量C++11/17的老项目,出于人力和风险的考虑并没有上新标准,我作为一个C++菜鸡,也没想着去了解C++20都更新了什么,直到最近遇到了很多语言层面的问题,于是想着学习一下C++20特性

当然这里面不止C++20

语言特性

指定初始化

可以只初始化一部分,可以指定想要初始化的部分

struct Student
{
int id = 0;
std::string name;
};

Student s { .name = "Jack" };

Lambda

[=]以前可以隐式获取this,现在需要改为[=, this]

// C++20以前
auto func = [](auto&& ...args)
{
return foo(std::forward<decltype(args)>(args)...);
}
// C++20以后
auto func = []<typename ...T>(T&& ...args)
{
return foo(std::forward(args)...);
}

for循环支持初始化

auto getData()
{
std::vector<int> a = {1, 2, 3, 4, 5};
return a;
}
int main() {
for(auto data = getData(); auto& v: data)
{
std::cout << v << std::endl;
}
return 0;
}

指引switch编译优化

switch (value) {
case 1: break;
[[likely]] case 2: break;
[[unlikely]] case 3: break;
}

设计理念

由于宏有各种各样的坏处,C++20开始,打算逐渐废除宏。然而宏在C++程序中其实非常普遍,很多写法高度依赖于此,C++标准委员会给了很多功能用于绕过宏

std::source_location

用于传递函数调用者的信息,比调用者的函数名、文件名

这是传统的使用宏写Log的方法,宏在这里还拼接传递了__FUNCTION____VA_ARGS__,使得输出中自动包含了函数名,而不需要手动传入

#include <iostream>
#include <string>
#include <format>

class Logger final
{
public:
static Logger& getInstance()
{
static Logger instance;
return instance;
}

template<typename... Args>
void log(Args&&... args)
{
(std::cout << ... << args) << std::endl;
}
};

#define LOG(...) Logger::getInstance().log(std::format("[{}] {}", __FUNCTION__, std::format(__VA_ARGS__)))

int main() {
LOG("Hello World!"); // [main] Hello World!
return 0;
}

这是C++20的写法,使用了std::source_location

#include <iostream>
#include <string>
#include <format>
#include <source_location>
#include <string_view>

class Logger final
{
public:
static void log2(const std::string_view message, const std::source_location location = std::source_location::current())
{
std::cout << '[' << location.function_name() << "] " << message << std::endl;
}

};

int main() {
Logger::log2("Hello World!"); // [int main()] Hello World!
return 0;
}

C++应用

预处理include

感觉不是很好用,建议不用

判断能不能include一个文件

// pch.h
#pragma once
#include <iostream>
#if __has_include("pch.h")
#include "pch.h"
#define NUMBER 1
#else
#include <iostream>
#define NUMBER 2
#endif

int main() {
std::cout << NUMBER << std::endl;
return 0;
}

Modules 模块

更高级的PCH,现阶段这东西大项目完全没法用

优点

  • 没有头文件,以及相关的依赖问题
  • 引入模块时不需要像头文件那样指定路径
  • 编译速度非常快(C++引用头文件会巨幅降低编译速度,于是很多人喜欢在头文件中只放一个类指针,到用到里面内容的时候在引用头文件,这样会大幅提高编译速度)
  • 显示指定导入导出
  • 模块引入顺序无关
  • 与现有的头文件兼容
  • 未来也许会像Python那样提供pip和包管理器?

缺点

  • 除了最新的MSVC,其他编译器都没怎么实现这个功能
  • MSVC自己擅自主张,不按C++标准改了很多东西
  • IDE不支持全局modules的提示和跳转(VS2022在2023年10月支持了,clion至今还是垃圾)
  • 需要反复引入std头文件(在C++23有快捷引入方式,但MSVC实现的很阴间)
  • 对第三方库极度不友好
  • 没有保存修改,IDE几乎没法解析,大多数情况IDE不给智能提示
  • 和头文件、宏混用时容易报错
  • 缺少std头文件时报错几乎无法阅读,大量匹配问题
  • cmake对其支持相当差
  • 有的编译器无法将.cpp识别为modules???
  • import是局部的,每一个文件都需要反复import modules
  • 一些静态变量、函数要到被用到时才会报错,build通过的代码不能保证能运行

用例

以MSVC的写法为例

// student.ixx
export module Student;
import <string>;
namespace Demo
{
export class Student
{
public:
Student();
~Student();
void display();
private:
std::string m_name;
}
}
// student.cppm
import Student; // MSVC擅自废弃了module Student的写法,很离谱
import <string>; // 反复import std也阴间了,C++23赶快端上来吧
import <iostream>;
namespace Demo
{
Student::Student(){
m_name = "Hello world";
}
Student::~Student(){}
Student::display()
{
std::cout << m_name << std::endl;
}
}

设计模式

记住单例就行

单例(Singleton)

也分懒汉式和饿汉式,最常用的设计模式,讲了很多次

class GameManager
{
public:
static GameManager* getInstance()
{
if (m_instance == nullptr)
{
m_instance = new GameManager();
}
return m_instance;
}
private:
static GameManager* m_instance;
GameManager() {};
};

工厂(Factory)

class Weapon
{
public:
Weapon(std::string name) : m_name(name) {}
private:
std::string m_name;
};

class WeaponFactory
{
public:
static Weapon* createWeapon(std::string name)
{
return new Weapon(name);
}
};

享元(Flyweight)

共享相似数据,对象仅拥有指向,下面的示例不同的享元类共用了工厂中的“颜色”

// 抽象享元类
class Shape {
public:
virtual void draw(int x, int y) const = 0;
};

// 具体享元类 - 圆形
class Circle : public Shape {
private:
std::string color; // 内部状态

public:
Circle(const std::string& color) : color(color) {}

void draw(int x, int y) const override {
std::cout << "Draw a " << color << " circle at position (" << x << ", " << y << ")" << std::endl;
}
};

// 具体享元类 - 矩形
class Rectangle : public Shape {
private:
std::string color; // 内部状态

public:
Rectangle(const std::string& color) : color(color) {}

void draw(int x, int y) const override {
std::cout << "Draw a " << color << " rectangle at position (" << x << ", " << y << ")" << std::endl;
}
};

// 享元工厂
class ShapeFactory {
private:
std::unordered_map<std::string, Shape*> shapes; // 享元对象池

public:
Shape* getShape(const std::string& color) {
if (shapes.find(color) == shapes.end()) {
// 如果池中没有该颜色的图形,创建并加入池中
if (color == "Red") {
shapes[color] = new Circle(color);
} else if (color == "Blue") {
shapes[color] = new Rectangle(color);
}
}
return shapes[color];
}
};

int main() {
ShapeFactory shapeFactory;

// 在不同位置绘制相同颜色的圆形和矩形
Shape* redCircle = shapeFactory.getShape("Red");
redCircle->draw(10, 10);

Shape* blueRectangle = shapeFactory.getShape("Blue");
blueRectangle->draw(20, 20);

// 再次在不同位置绘制相同颜色的圆形
Shape* anotherRedCircle = shapeFactory.getShape("Red");
anotherRedCircle->draw(30, 30);

// 注意:虽然外部状态不同,但相同颜色的圆形共享内部状态,从而减少了对象的创建
// 这符合享元模式的思想,通过共享尽可能多的相似对象来减少内存或计算开销

return 0;
}

观察者(Observer)

状态机(State)

代理(Proxy)

装饰者(Decorator)

迭代器(Iterator)

适配器(Adapter)

命令(Command)

面向对象

模板元

除非是库代码,不然用模板元去优化业务代码,属实是往代码里下毒

更详细的信息可以看C++模板

Traits

为了减少相似代码,实现泛型,我们需要模板

对于一些特殊类型,我们往往需要特殊对待,于是需要模板特化

于是我们需要知道类型的特征,这就是Type Traits的作用

C++ STL中 Type Traits是一堆形如IsXxxx<T>::value的东西

原理

下面是boost库一些traits实现

创建一个默认的行为和一个特例,仅当模板类型是特例时,value才是true

template< typename T >
struct is_void { static const bool value = false; };

template<>
struct is_void< void >{ const bool value = true; };
template< typename T >
struct is_pointer { static const bool value = false; };

template< typename T >
struct is_pointer< T* >{ static const bool value = true; };

使用

auto ans = is_void<T>::value;
auto ans2 = is_pointer<T>::value;

std::true_type与std::false_type

下面是通过类型萃取,判断一个类中是否有某个成员函数

#include <type_traits>

// 定义一个支持Check的类
struct Check {
void Execute() {
std::cout << "Check::someFunction() called." << std::endl;
}
};

// 定义一个不支持Check的类
struct NoCheck {
// 没有someFunction成员函数
};


template <typename T>
struct hasFunction_Execute {
private:
template <typename U>
static auto check(U* ptr) -> decltype(std::declval<U>().Execute(), std::true_type{});

template <typename U>
static std::false_type check(...);

public:
static constexpr bool value = decltype(check<T>(nullptr))::value;
};

int main() {
if (hasFunction_Execute<Check>::value) {
Check obj;
obj.Execute();
}
// 测试NoCheck类
if (hasFunction_Execute<NoCheck>::value) {
// 不会执行到这里,因为NoCheck类没有someFunction成员函数
}
}

TMP

template metaprogramming(TMP):将其他程序的代码视为数据的编程技术

TMP通常意味着代码可以被其他程序读取、生成、分析、转化,甚至运行时修改

C++ TMP是一种用编译速度和存储空间换运行时速度的技术,将计算放在编译期,可以提高运行时性能

递归

C++ TMP很适合写递归,比如计算阶乘

constexpr unsigned long Factorial (unsigned long X)
{
return (X == 0) ? 1 : X * Factorial(X - 1);
}

int main()
{
constexpr auto x = Factorial(4);
std::cout << x << std::endl; // 24
return 0;
}

编译为汇编,发现程序并没有调用Factorial函数,而是直接找了一个立即数24

构造函数

constexpr也可以修饰构造函数,创建编译器常量,以提高性能

使用该功能需要构造函数体的简单性,比如不能有虚函数,不能使用动态内存分配,条件语句,成员必须使用常量表达式初始化,递归必须在编译器能中止

class MyClass {
public:
constexpr MyClass(int x) : value(x) {}

int getValue() const { return value; }

private:
int value;
};

int main()
{
constexpr MyClass obj1(42);
std::cout << obj1.getValue() << std::endl;
return 0;
}

字符串

std::string_view

可以看作一个封装好的轻量级const std::string&,只读、不拥有字符串所有权,性能很好,非常推荐使用

#include <string_view>

void print_string_view(std::string_view str) {
std::cout << str << std::endl;
}

int main() {
std::string_view str_view = "Hello, World!";
print_string_view(str_view);
std::string str = "Hello, World!";
print_string_view(str);
print_string_view("Hello, World!"); // 字符串常量字面量是一个左值,生命周期和整个程序一致,所以可以放心传
return 0;
}

STL

内存分配

<memory>库提供了两个基础函数,allocate()deallocate()

std::allocator<int> alloc;
// 开辟一个长度为10个int的空间
int* p = alloc.allocate(10);
// 将数组第一个值构设为42
std::allocator_traits<std::allocator<int>>::construct(alloc, p, 42);
// 销毁这个数组
alloc.deallocate(p, 10);

concept 迭代器

C++迭代器已经是要被淘汰的概念了,但是新的C++标准仍然在对迭代器进行拓展

concept可以对迭代器进行约束(也就是提要求requires-expression

#include <iostream>
#include <vector>
#include <iterator>
#include <concepts>

template <typename Iter>
concept InputIterator = requires(Iter iter) {
{ iter++ } -> std::same_as<Iter>; // 要求自增
{ iter != iter } -> std::convertible_to<bool>; // 要求能比较不等
{ ++iter } -> std::same_as<Iter&>; // 要求迭代器能前进
};

template <InputIterator Iter>
void printElements(Iter begin, Iter end) {
while (begin != end) {
std::cout << *begin << ' ';
++begin;
}
std::cout << '\n';
}

int main() {
std::vector<int> numbers = { 1, 2, 3, 4, 5 };
printElements(numbers.begin(), numbers.end()); // 若不满足会编译报错

return 0;
}

函数式编程

函数式编程将程序分解为函数(而非对象),使用表达式进行操作(而非语句)

函数式编程的核心是将一个大问题切成多个小问题

函数式编程更适合多线程

std::function

函数式编程将函数视为一等公民,常常将函数作为参数进行传递。不过一般公民通常需要有自己的成员和状态,函数指针无法满足我们的需求,重载类的()运算符有点太OOP了

C++提供了高阶函数:

#include <functional>

std::function<int(int, int)> get_multiplier()
{
return [](int a, int b) { return a * b; };
}

std::function<int(int)> multiply(int a)
{
return [a](int b) { return a * b; };
}

int main()
{
auto mul = get_multiplier();
std::cout << mul(10, 20) << std::endl;
std::cout << multiply(10)(20) << std::endl;
auto mul3 = multiply(10);
std::cout << mul3(20) << std::endl;
}

Ranges 范围

感觉不同编译器对这个功能的支持差异好大

  • 为std容器提供了简洁、强大、顺序可控的操作方式
  • 引入view概念,运行创建一个惰性计算操作,不立即得到结果,提高内存利用率和性能
  • 使用|->操作符处理范围变量
// 查找array中偶数的数量
std::array<int, 5> arr = {1, 2, 3, 4, 5};
auto res = std::count_if(arr.begin(), arr.end(), [](int x) { return x % 2 == 0; }); // 2 个偶数

auto res2 = arr | std::views::filter([](int x) { return x % 2 == 0; });
for (auto i : res2) {
std::cout << i << std::endl; // 输出 2 4
}

transform和filter返回的是一个range结构,而非容器

// 将vector中所有元素做平方,并去掉奇数项
std::vector<int> data { 0, 1, 2, 3, 4, 5 };
auto square = [](int i) { return i * i; };
auto odd = [](int i){ return i % 2 != 0; };
auto result = data | std::views::transform(square) | std::views::filter(odd); // 1 9 25
// 将一个二维数组展成一维
std::vector<std::vector<int>> v2 = {
{1, 2, 3},
{4, 5},
{6, 7, 8, 9}
};
auto result2 = v2 | std::views::join;
// 创建一个从1开始递增的数组,并对其进行平方,取前十个数(最后实际也只会计算十个数)
auto square = [](int i) { return i * i; };
auto res = std::views::iota(1) | std::views::transform(square)| std::views::take(10);
for (auto i : res) {
std::cout << i << " "; // 1 4 9 16 25 36 49 64 81 100
}

纯函数

pure

纯函数:不改变程序状态的函数

纯函数给定输入,无论执行多少次,都返回相同的结果

传函数不访问全局变量,仅使用输入的参数

折叠

折叠:将一组数据整合为数量更少的数据的方式

#include <numeric>
int main()
{
std::vector<int> numbers = { 1, 2, 3, 4, 5 };
// 一个累乘的示例
int product = std::accumulate(numbers.begin(), numbers.end(), 1, std::multiplies<int>());
std::cout << "Product of elements: " << product << std::endl;
}

std::multiplies<int>()可以替换为lambda表达式

并发和多线程

感觉不如TBB

std::thread

thread构造时就会启动线程

#include <thread>

void print_background()
{
auto i{ 0 };
while (true)
{
std::cout << "Background: " << i++ << std::endl;
}
}

int main() {
std::thread background{print_background};
auto j{ 0 };
while (j < 1000)
{
std::cout << "Main: " << j++ << std::endl;
}
}

join

等待线程停止

int main() 
{
std::thread tl
{
[] { std::cout << "A lambda passed to the thread" << std::endl; }
};
tl.join(); // 等待 tl执行结束
std::cout << "Main thread" << std::endl;
}

线程结束后,与线程关联都是对象tl状态会被改变,此时调用tl.joinable()会返回false,你可以用joinable()来判断对象是否还在持有一个线程

detach

极其不推荐使用,多线程编程应该自行管理资源

分离线程,线程对象将放弃对线程资源的所有权,线程将独立运行,结束后自动释放所有分配的资源

tl.detach();

传参

构造时传参,这下参数会被传递到新线程的内存空间中

注意,即使参数标为引用,也是复制

void foo(int a, int b) {...}

void foo2(int, const int& a) {...}

int main()
{
std::thread t1{ foo, 1, 2 };
int n = 4;
std::thread t2{ foo2, 3, n };
}

使用成员函数指针

成员函数指针也可以传给thread构造,自然也可以使用std::bind

struct Utils{
void task_run(int)const;
};

Utils u;
int n = 0;
std::thread t{ &Utils::task_run, &u,n };
std::thread t2{ std::bind(&Utils::task_run, &u ,n) };
t.join();

成员指针必须和对象一起使用,不可以转换到函数指针单独使用

std::this_thread

用于管理当前线程

get_id

打印当前线程id

int main()
{
std::thread t{ [] {
std::cout << std::this_thread::get_id() << '\n';
} };
t.join();
return 0;
}

sleep_for

休眠一段时间

#include <thread>
#include <iomanip>

int main()
{
std::thread t{ [] {
// 打印休眠前时间戳
auto now = std::chrono::system_clock::now();
auto now_time = std::chrono::system_clock::to_time_t(now);
std::cout << "Current time:\t\t" << std::put_time(std::localtime(&now_time), "%H:%M:%S") << std::endl;
// 休眠2秒
std::this_thread::sleep_for(std::chrono::seconds(2));
// 打印休眠后时间戳
now = std::chrono::system_clock::now();
now_time = std::chrono::system_clock::to_time_t(now);
std::cout << "Current time:\t\t" << std::put_time(std::localtime(&now_time), "%H:%M:%S") << std::endl;
} };
t.join();
return 0;
}

使用using namespace std::chrono_literals3s等同于std::chrono::seconds(3)

sleep_until

休眠到具体某个时间戳

using namespace std::chrono_literals;

int main()
{
std::thread t{ [] {
// 打印休眠前时间戳
auto now = std::chrono::system_clock::now();
auto now_time = std::chrono::system_clock::to_time_t(now);
std::cout << "Current time:\t\t" << std::put_time(std::localtime(&now_time), "%H:%M:%S") << std::endl;
// 休眠到3秒以后
auto wakeup_time = now + 3s;
std::this_thread::sleep_until(wakeup_time);
// 打印休眠后时间戳
now = std::chrono::system_clock::now();
now_time = std::chrono::system_clock::to_time_t(now);
std::cout << "Current time:\t\t" << std::put_time(std::localtime(&now_time), "%H:%M:%S") << std::endl;
} };
t.join();
return 0;
}

yield

// 循环等待isDone
while (!isDone()){
// 使用yield可以减少CPU浪费,不会一直在这里高频空转
std::this_thread::yield();
}

std::jthread

C++20提供的新线程库,可以完美替代std::thread

std::jthread在线程运行结束后再进行析构,可以确保线程安全退出

对异常和中断的支持更好

RAII

jthread与thread的一大不同:jthread在析构时会调用join()

主动停止

jthread可以主动请求某个线程结束,下面的代码在Main输出完毕后background也会停止输出

void print_background(std::stop_token stoken)
{
auto i{ 0 };
while (true)
{
if (stoken.stop_requested())
{
return;
}
std::cout << "Background: " << i++ << std::endl;
}
}

int main()
{
std::jthread background{print_background};
std::stop_token stoken = background.get_stop_token();
auto jx{ 0 };
while (jx < 1000)
{
std::cout << "Main: " << jx++ << std::endl;
}
background.request_stop(); // 请求结束
}

互斥信号量

多线程我们不可避免遇到线程竞争,尤其是读写

lock

std::mutex locker;

int global_value{ 0 };

void inc()
{
std::lock_guard g(locker); // 通过生命周期管理锁
global_value++;
std::cout << global_value << std::endl;
}

int main()
{
std::thread t1{ inc };
std::thread t2{ inc };
}

try_lock

lock会立刻上锁,我们也可以进行尝试上锁,若锁已经被其他线程锁住,当前线程会立刻返回,可以去做其他事,而不是阻塞在这里

std::mutex locker;

void thread_function(int id) {
// 尝试加锁
if (locker.try_lock()) {
std::cout << "线程:" << id << " 获得锁" << std::endl;
// 临界区代码
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟临界区操作
locker.unlock(); // 解锁
std::cout << "线程:" << id << " 释放锁" << std::endl;
} else {
std::cout << "线程:" << id << " 获取锁失败 处理步骤" << std::endl;
}
}

保护

不要将受保护数据传递给外部(比如通过指针、引用),这样保护就失去意义了

死锁

多个互斥信号量可能会导致死锁

std::unique_lock

同步

future:在等待某一件事时做另一件事

std::future

与一个事件相关联

#include <thread>
#include <future>

int task(int n) {
std::cout << "异步任务 ID: " << std::this_thread::get_id() << '\n';
return n * n;
}

int main() {
std::future<int> f = std::async(task, 10);
std::cout << "main: " << std::this_thread::get_id() << '\n';
std::cout << std::boolalpha << f.valid() << '\n'; // true
std::cout << future.get() << '\n';
std::cout << std::boolalpha << f.valid() << '\n'; // false
}

std::shared_future

能关联多个事件

std::async

  • deferred:惰性求值,不创建线程,等待调用wait、get再执行任务
  • async:在不同线程
void f(){...}
// 这里的 auto 是 std::future<void>
auto f1 = std::async(std::launch::deferred, f); // 此时f1并不会执行
f1.wait(); // 主动让f1执行
auto f2 = std::async(std::launch::async,f); // 立刻开一个线程执行f2

注意,std::future的析构会阻塞std::async,因此匿名Lambda并不能用了创建异步任务

std::async(std::launch::async, []{ f(); }); // 临时量的析构函数等待 f()
std::async(std::launch::async, []{ g(); }); // f() 完成前不开始

协程

C++20引入了关键词co_awaitco_yieldco_return,但是不能用,要到C++23以后才有相关的库

参考

C++11、14、17、20新增内容

《Expert C++》

CppReference

评论