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]
auto func = [](auto&& ...args) { return foo(std::forward<decltype(args)>(args)...); }
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!"); 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!"); return 0; }
|
C++应用
预处理include
感觉不是很好用,建议不用
判断能不能include一个文件
#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的写法为例
export module Student; import <string>; namespace Demo { export class Student { public: Student(); ~Student(); void display(); private: std::string m_name; } }
|
import Student; import <string>; 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>
struct Check { void Execute() { std::cout << "Check::someFunction() called." << std::endl; } };
struct NoCheck { };
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(); } if (hasFunction_Execute<NoCheck>::value) { } }
|
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; 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;
int* p = alloc.allocate(10);
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概念,运行创建一个惰性计算操作,不立即得到结果,提高内存利用率和性能
- 使用
|
和->
操作符处理范围变量
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; });
auto res2 = arr | std::views::filter([](int x) { return x % 2 == 0; }); for (auto i : res2) { std::cout << i << std::endl; }
|
transform和filter返回的是一个range结构,而非容器
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);
|
std::vector<std::vector<int>> v2 = { {1, 2, 3}, {4, 5}, {6, 7, 8, 9} }; auto result2 = v2 | std::views::join;
|
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 << " "; }
|
纯函数
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(); std::cout << "Main thread" << std::endl; }
|
线程结束后,与线程关联都是对象tl
状态会被改变,此时调用tl.joinable()
会返回false
,你可以用joinable()
来判断对象是否还在持有一个线程
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; 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_literals
,3s
等同于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; 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
while (!isDone()){ 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'; std::cout << future.get() << '\n'; std::cout << std::boolalpha << f.valid() << '\n'; }
|
std::shared_future
能关联多个事件
std::async
- deferred:惰性求值,不创建线程,等待调用wait、get再执行任务
- async:在不同线程
void f(){...}
auto f1 = std::async(std::launch::deferred, f); f1.wait(); auto f2 = std::async(std::launch::async,f);
|
注意,std::future
的析构会阻塞std::async
,因此匿名Lambda并不能用了创建异步任务
std::async(std::launch::async, []{ f(); }); std::async(std::launch::async, []{ g(); });
|
协程
C++20引入了关键词co_await
、co_yield
、co_return
,但是不能用,要到C++23以后才有相关的库
参考
C++11、14、17、20新增内容
《Expert C++》
CppReference