Modern C++ (C++11 onwards) introduces features like smart pointers, move semantics, lambda expressions, and constexpr for improved safety, readability, and performance. Newer additions like Concepts, Ranges, and std::format in C++20 further enhance template programming, data manipulation, and string formatting, making C++ development more efficient and user-friendly.
所谓 Modern C++ 指的是从 C++11 开始引入的一系列新特性和改进, 相比 C++98/03 引入了非常多的语法和特性. 在这里将会作为附录简要介绍一些常用的 Modern C++ 特性, 以帮助读者更好地理解和使用 C++ 进行开发. Modern C++ 的主要目标是提高代码的类型安全性, 可读性和性能.
未定义行为(a.k.a Undefined Behavior, UB), C++ 中不得不品的一环. 由于 C++ 大佬众多, 编译器实现也是五花八门, 因此 C++ 标准中规定了一些行为是未定义的, 也就是说编译器可以自由选择如何处理这些行为, 包括忽略它们, 抛出异常, 崩溃等. 通常情况下编译器在进行优化的时候完全不会考虑未定义行为是否符合原始意图, 它们也无需保证这一点. 因此在编写 C++ 代码时, 一定要避免写出未定义行为. 常见的比如调试编译能正常工作, 发布构建就崩溃了几乎就是因为 UB 引起的.
不幸的是, 关于 UB 行为的列表非常长, 有的时候只能靠查标准文档来确定某个行为是否 UB.
但 UB 也并不尽是坏处, 例如大名鼎鼎的 container_of 宏就是个 UB, 它在 Linux 内核中被广泛使用, 典型实现如下:
#define offsetof(type, member) ((size_t) &((type *)0)->member)
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (const typeof( ((type *)0)->member ) *)(ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})这里面的 UB 点在于对空指针进行解引用, 但是这个宏的实现非常高效. 不过话又说回来, 人家这是 Linux 内核, 普通选手还是别自创了.
右值引用是 C++11 引入的一种新的引用类型, 用来表示可以被移动的对象. 右值引用使用 && 符号表示, 例如:
int &&rvalueRef = 5; // rvalueRef is a rvalue reference to an integer什么叫做右值? 顾名思义, 右值是指那些不能出现在赋值语句左边的表达式, 通常是临时对象或者字面值. 例如上面的 5 就是一个右值.
右值引用的主要用途是实现移动语义, 允许资源的所有权从一个对象转移到另一个对象, 而不是进行传统的深拷贝. 例如:
#include <vector>
#include <utility> // for std::move
std::vector<int> vec1 = {1, 2, 3, 4, 5};
std::vector<int> vec2 = std::move(vec1); // move vec1 to vec2vec1 首先会申请一块内存, 储存初始的数据, 然后通过 std::move 将 vec1 的资源所有权转移给 vec2, 现在这块内存就归
vec2 所有了, 资源的释放由 vec2 负责.
C++11 中和右值引用配套引入的 std::move 函数, 是用来用来将一个左值转换为右值引用, 以便触发移动语义. 需要注意的是,
std::move 并不会真的移动任何东西, 它只是一个类型转换工具(名字起的并不是很好, 比如 rvalue_cast可能还更好一些).
在上面这个例子中, vec1 被移动之后, 原先所指向的那块内存上的数据其实没有发生改变, 只是内存的归属发生了变化. 那理论上似乎还能通过
vec1 访问原始的数据. 实际上绝对不推荐这么做, 按照 C++ 标准这是一个未定义行为, 强行这么做可能发生任何事情, 比如段错误.
所以一个对象在被移动之后, 不可以再通过这个对象访问其管理的内存了.
为了给你自定义的类添加移动语义, 需要定义一个移动构造函数和一个移动赋值运算符. 例如:
class MyClass {
public:
MyClass(size_t size) : size_(size) {
data_ = new int[size_];
}
// move constructor
MyClass(MyClass &&other) noexcept : data_(other.data_), size_(other.size_) {
// 通常情况下需要将 other 置于一个有效但空的状态
other.data_ = nullptr; // leave other in a valid state
other.size_ = 0;
}
// move assignment operator
MyClass &operator=(MyClass &&other) noexcept {
if (this != &other) {
delete[] data_; // release current resource
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr; // leave other in a valid state
other.size_ = 0;
}
return *this;
}
~MyClass() {
delete[] data_;
}
private:
int *data_;
size_t size_;
}这样就为 MyClass 添加了移动语义, 允许通过右值引用来移动对象的资源, 在很多大规模数据结构中可以显著提高性能,
避免不必要的深拷贝开销.
这里用到了一个 noexcept 关键字, 用来表示这个函数不会抛出异常. 通常情况下移动构造函数和移动赋值运算符都应该标记为
noexcept, 原因这里暂且不提.
引用折叠(Reference Collapsing)是引入右值后必然出现的一个规则. 它描述了当引用类型被引用时, 引用类型如何折叠成最终的引用类型. 规则如下:
T& & 折叠为 T&T& && 折叠为 T&T&& & 折叠为 T&T&& && 折叠为 T&&这里的 T 可以是任何类型, 包括基本类型和用户自定义类型. 这是什么意思呢? 举个例子:
template<typename T>
void func(T &¶m) { // param is a universal reference
// ...
}
int x = 10;
func(x); // T is deduced as int&, param is int&&
func(20); // T is deduced as int, param is int&&在函数的参数中, param 被声明为一个 T&&, 根据折叠规则, 当传入一个左值的时候, T 会被推导为 int&; 当传入一个右值的时候,
T 会被推导为 int. 这样 param 的类型会根据传入的实参类型而变化, 而 T&& 被称为万能引用(Universal Reference)
或者转发引用(Forwarding Reference).
对于引用折叠的前两条规则, 例子如下
template<typename T>
void func(T ¶m) { // param is an lvalue reference
// ...
}
int x = 10;
func(x); // T is deduced as int, param is int&
func(20); // error: cannot bind rvalue to lvalue reference根据折叠规则, 此时 param 永远一个左值引用, 尝试传入一个右值是非法的.
由于左值和右值的复杂性, 以及各种类型修饰符(例如const), 很多时候我们希望能够将函数的参数带上它们完完整整的类型信息传递给另一个函数,
这被称为完美转发(Perfect Forwarding). 这个功能由标准库中的 std::forward 函数实现, 例如:
#include <utility> // for std::forward
template<typename T, typename... Args>
void wrapper(Args&&... args) {
// do some pre-processing
// then forward the arguments to another function
func(std::forward<Args>(args)...); // perfect forwarding
}完美转发通常会配合万能引用一起使用, 以确保传递给 func 的参数保持原始的引用类型. 这种技术一般还会配合模版变参一起使用,
以支持任意数量和类型的参数. 关于模板变参, 下面有提及.
C++11 最伟大的改进之一就是引入了智能指针, 用来自动管理动态分配的内存资源, 避免内存泄漏和悬空指针等问题. 传统的指针需要手动释放内存, 很多时候甚至不是说忘记释放的问题, 是不知道什么时候释放是万无一失的, 释放早了就会段错误, 或者悬空指针. 总之裸指针在 Modern C++ 中是不推荐使用的.
C++11 引入了两种主要的智能指针: std::unique_ptr 和 std::shared_ptr.
std::unique_ptr 用来表示独占所有权的智能指针, 一个 unique_ptr 对象拥有其所指向的内存资源的唯一所有权, 它不能被复制,
只能被移动. 例如:
#include <memory>
int *ptr = new int[100]; // allocate memory
std::unique_ptr<int[]> p1(ptr); // wrap it in a unique_ptr
// std::unique_ptr<int[]> p2 = p1; // error: cannot copy unique_ptr
std::unique_ptr<int[]> p2 = std::move(p1); // move ownership to p2这里我们首先分配了一块动态内存, 然后将这块内存交给一个 std::unique_ptr 来管理, 它会在析构时自动释放内存. 由于
unique_ptr 不能被复制, 因此第五行尝试将 p1 赋值给 p2 会报错, 一块内存只能由一个智能指针对象进行管理. 第六行使用
std::move 将 p1 的所有权转移给 p2, 现在 p2 负责释放这块内存了, 而 p1 变成了一个空指针.
在现代编程语言中, 所有权是一个很重要的概念, 尤其是 Rust, 它将所有权作为语言的核心概念来设计内存管理. C++11 引入智能指针也是为了更好地管理内存资源, 避免手动管理内存带来的各种问题.
std::shared_ptr 用来表示共享所有权的智能指针, 多个 shared_ptr 对象可以指向同一块内存资源, 通过引用计数(Reference
Count)来管理内存的释放. 例如:
#include <memory>
std::shared_ptr<int> p1(new int(42)); // create a shared_ptr
std::shared_ptr<int> p2 = p1; // copy p1 to p2
// now both p1 and p2 share ownership of the same memory这里我们创建了一个 std::shared_ptr 对象 p1, 然后将它赋值给 p2, 现在 p1 和 p2 都指向同一块内存, 它们共享这块内存的所有权.
内部的引用计数会在每个 shared_ptr 对象被创建时增加, 在对象被销毁时减少, 当引用计数变为零时, 内存才会被释放.
shared_ptr 大多用在多线程环境下, 也就是多个线程可能会共享同一块内存资源的场景. 需要注意的是, shared_ptr 的引用计数是线程安全的,
但对同一块内存的读写操作仍然需要用户自己保证线程安全.
标准库也提供了创建智能指针的标准方式, std::make_unique 和 std::make_shared, 它们等价于上面的 new 操作, 但更安全和高效.
例如:
auto p1 = std::make_unique<int[]>(100); // create a unique_ptr
auto p2 = std::make_unique<MyStruct>(); // create a unique_ptr to MyStruct
auto p3 = std::make_shared<int>(42); // create a shared_ptr其实还有一种智能指针是 std::weak_ptr, 它用来解决孤岛引用(Reference Cycle)的问题, 比如两个对象互相持有对方的
shared_ptr, 导致引用计数永远不会变为零, 内存永远不会被释放. weak_ptr 不会增加引用计数, 因此可以打破这种循环引用.
这里就不展开讲了, 有兴趣的读者可以自行查阅相关资料.
关于智能指针的性能问题似乎一直有争议, 有些人觉得性能就是不如裸指针, 但至少对于我用的比较多的 std::unique_ptr, 它在
-O2 优化下就是零开销抽象, 编译器会把它优化成和裸指针一样的代码, 大多数情况下也不需要担心这一点性能开销.
decltype 和 自动类型推导 (C++11)decltype 是 C++11 引入的一个关键字, 用来获取表达式的类型. 它可以用在变量声明, 函数返回类型等地方, 允许编译器根据已有的表达式推导出类型,
例如:
int x = 42;
decltype(x) y = x; // y is of type int也可以用在函数返回类型上, 例如:
template<typename T1, typename T2>
auto add(T1 a, T2 b) -> decltype(a + b) {
return a + b;
}编译器会自动根据模板特化时传入的参数类型推导出 a + b 的结果类型, 作为函数的返回类型.
另外, C++11 还引入了 auto 关键字, 用来进行自动类型推导. 使用 auto 声明的变量, 编译器会根据初始化表达式的类型自动推导出变量的类型,
例如:
auto x = 42; // x is of type int
auto y = 3.14; // y is of type double
auto vec = std::vector<int>{1, 2, 3}; // vec is of type std::vector<int>auto 关键字可以大大简化代码, 尤其是在处理复杂类型时, 例如迭代器类型等. 例如:
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
// ...
}没有 auto 的话, 你需要写出完整的迭代器类型, 就像 std::vector<int>::iterator it = vec.begin(); 那样, 代码会显得冗长且难以阅读.
另一个初见有点匪夷所思的 auto 的用法是作为函数参数类型, 例如:
void foo(auto &&a, auto &&b) {
// ...
}这里实际上等价于定义了函数模板:
template<typename T1, typename T2>
void foo(T1 &&a, T2 &&b) {
// ...
}这种用法可以让函数接受任意类型的参数, 并且结合右值引用实现完美转发(Perfect Forwarding). 这种用法在 C++20 中引入, 称为简写函数模板(Abbreviated Function Templates).
auto 也可以和 decltype 结合使用, 例如:
template<typename T1, typename T2>
auto add(T1 &&a, T2 &&b) -> decltype(std::forward<T1>(a) + std::forward<T2>(b)) {
return std::forward<T1>(a) + std::forward<T2>(b);
}这里使用 decltype 来获取 a + b 的结果类型, 结合 std::forward 实现完美转发.
另一种方式是 decltype(auto) 关键字, 用来表示根据表达式的类型推导出变量的类型, 在看例子之前, 我们先要知道 auto 的局限性,
auto 在推导类型时会忽略掉引用和 const 修饰符, 除非显式地指定 const auto 或是 auto &. 但是有时候我们希望保留这些修饰符,
这时候就可以使用 decltype(auto). 例如:
const int x = 42;
auto y = x; // y is of type int, const is ignored
decltype(auto) z = x; // z is of type const int, const is preserved利用这个可以实现例如容器的元素访问函数, 保留元素的引用和 const 修饰符:
template<typename Container>
decltype(auto) getElement(Container &&c, size_t index) {
// if c is an lvalue (vector<int>&), returns int&
// if c is an rvalue (vector<int>&&), returns int
return std::forward<Container>(c)[index];
}
std::vector<int> vec = {1, 2, 3};
decltype(auto) elem1 = getElement(vec, 0); // elem1 is int&
decltype(auto) elem2 = getElement(std::vector<int>{4, 5, 6}, 0); // elem2 is int模板变参(Variadic Templates) 是 C++11 引入的一种强大的模板编程技术, 允许定义接受可变数量模板参数的模板类和函数.
这使得模板编程更加灵活和通用, 可以处理任意数量和类型的参数. 传统做法是使用宏或者递归模板来实现类似的功能, 或者直接
void* 大法. 但这些方法要么不够类型安全, 要么代码复杂且难以维护. 模板变参提供了一种更优雅和类型安全的解决方案. 例如,
下面是 MLIR 中常见的用来创建新操作的模板函数, 根据要创建的操作类型不同, 接受不同数量和类型的参数:
template<class OpType, typename... Args>
OpType OpBuilder::create<OpType>(Location loc, Args&&... args) {
return OpType::create(loc, std::forward<Args>(args)...);
}第一个模板参数 OpType 是要创建的操作类型, 后面的 Args... 是一个模板参数包, 可以接受任意数量和类型的参数. 在函数体内,
使用 std::forward<Args>(args)... 将参数完美转发给操作的静态创建方法.
比如我调用这样一个函数:
OpBuilder builder;
Location loc = ...;
auto allocOp = builder.create<AllocOp>(loc, /* MemRefType */ type, /* Attribute */ attr);那么 Args... 会被推导为 MemRefType, Attribute, args... 会被推导为对应类型的参数列表.
直观来讲编译器会创建这样一个模板特化:
OpBuilder::create<AllocOp, MemRefType, Attribute>(Location loc, MemRefType&& type, Attribute&& attr) {
return AllocOp::create(loc, std::forward<MemRefType>(type), std::forward<Attribute>(attr));
}Lambda 表达式是 C++11 引入的一种轻量级的匿名函数定义方式, 允许在代码中直接定义和使用函数对象. Lambda 表达式的语法如下:
[capture](parameters) -> return_type {
// function body
}capture 用来指定 Lambda 表达式可以访问的外部变量, 可以是值捕获, 引用捕获, 或者混合捕获. 只有被捕获的变量才能在函数体内使用.parameters 是函数的参数列表, 和普通函数一样.return_type 是返回类型, 可以省略, 编译器可以自动推导.capture 有几种常见的写法, = 表示按值捕获所有外部变量, & 表示按引用捕获所有外部变量, 也可以指定具体的变量进行捕获,
例如 [x, &y] 表示按值捕获 x, 按引用捕获 y. 例如:
int x = 10;
auto lambda = [x](int y) -> int {
return x + y;
};
int result = lambda(5); // result is 15Lambda 表达式的一个特点就是它是一个正儿八经的对象, 可以被赋值给变量, 传递给函数, 作为返回值, 从而实现函数式编程风格. 例如:
#include <algorithm>
#include <vector>
std::vector<int> vec = {1, 2, 3, 4, 5};
std::for_each(vec.begin(), vec.end(), [](int &n) {
n *= 2; // double each element
});在上面的例子中, 我们传一个 Lambda 表达式给 std::for_each 函数, 后者会对容器中的每个元素调用这个 Lambda 表达式,
实现对每个元素的操作.
很多时候 Lambda 表达式可以替代传统的函数对象(functor)和回调函数(callback), 使代码更加简洁和易读. 除了说 Lambda 能现定义现用之外, 另一个好处是它是类型安全的, 传统的函数指针法中, 传递的函数指针可能并不匹配预期的签名, 导致运行时错误. 而 Lambda 表达式在编译时就会进行类型检查, 确保传递的参数和返回值类型正确.
constexpr, consteval (C++11/C++17/C++23)伟大无需多言, 编译期计算的支柱, 模板元编程的基础(这可以说是 Modern C++ 性能上相比其它语言的最大优势之一). constexpr
用来修饰函数和变量, 表示它们可以在编译期进行求值. 例如:
constexpr int square(int x) {
return x * x;
}
constexpr int value = square(5); // value is 25, computed at compile time在这个例子中, square 函数被标记为 constexpr, 编译器知道它可以在编译期进行求值. 因此 value 变量的值在编译期就被计算出来了.
所以 constexpr 函数一个重要的应用场景就是打表, 不要看不起打表, 很多复杂计算都可以通过打表来极大提升性能, 因为在运行期都不用算,
直接查表就行了(相比而言, 编译期花的时间通常没那么重要).
constexpr 定义的变量还可以用来定义数组的大小(众所周知 C/C++ 中数组大小必须是编译期常量):
constexpr int size = 10;
int arr[size]; // valid, size is a compile-time constant这样就可以在编译期进行一大堆计算, 然后推导出需要分配多少资源. 例如 Eigen 库中是会根据你定义的矩阵大小和尺寸,
推导出需要分配多少内存的(比如临时变量在栈上分配会不会爆栈?), 这样在编译期间能发现很多潜在的错误, 总比运行时异常好.
consteval 用来定义一个必须在编译期求值的函数, 也就是说这个函数只能在编译期被调用, 不能在运行时调用. 例如:
// force compile-time evaluation
consteval size_t compile_time_hash(const char* str) {
size_t hash = 5381;
while (*str) {
hash = ((hash << 5) + hash) + *str++; // djb2 hash
}
return hash;
}
constexpr size_t USER_ID_HASH = compile_time_hash("user_id");
void runtime_input_test(const char* input) {
// 错误!因为 compile_time_hash 是 consteval,它不能在运行时调用。
// 这提供了极强的安全性保证:如果哈希值需要在运行时计算,则说明
// 程序员代码有逻辑错误或使用了不适当的输入。
// size_t runtime_hash = compile_time_hash(input);
}在这个例子中, compile_time_hash 函数被标记为 consteval, 因此它只能在编译期被调用. 如果你尝试在运行时调用它, 则编译器会报错,
你可能需要重新考虑你的设计. 这种机制可以帮助你在编译期捕获一些代码编写上的逻辑错误, 提高代码的安全性和正确性.
从逻辑上讲, consteval 函数内部可以调用 constexpr 函数, 但 constexpr 函数不能调用 consteval 函数, 因为后者必须在编译期求值,
前者只是告诉编译器它可以在编译期求值.
if constexpr 是 C++17 引入的一个编译期条件语句, 用来在编译期根据条件选择不同的代码路径. 例如:
template<typename T>
void print_type_info() {
if constexpr (std::is_integral_v<T>) {
std::cout << "T is an integral type." << std::endl;
} else {
std::cout << "T is not an integral type." << std::endl;
}
}在这个例子中, if constexpr 根据模板参数 T 是否是整数类型, 在编译期选择不同的代码路径. 例如:
print_type_info<int>();
// will specialize to:
void print_type_info<int>() {
std::cout << "T is an integral type." << std::endl;
}
print_type_info<double>();
// will specialize to:
void print_type_info<double>() {
std::cout << "T is not an integral type." << std::endl;
}自然, if constexpr 只能判断那些在编译期就能确定的条件, 不能用来判断运行时的条件. 它的主要用途是实现模板元编程,
根据模板参数的不同生成不同的代码.
if constexpr 的一个重要特点是, 它的分支代码在编译期不会被实例化, 也就是说, 如果某个分支的代码在编译期条件下不会被执行,
那么编译器不会对该分支进行类型检查和语法检查. 这使得我们可以在模板中编写一些只有在特定条件下才有效的代码, 而不会导致编译错误.
这是很有用的, 看下面这个例子:
#include <iostream>
#include <type_traits>
// check if T supports ostream operator<<
template<typename T>
struct supports_ostream {
private:
// try to instantiate this function template
template<typename U>
static auto test(U* u) -> decltype(std::cout << *u, std::true_type{});
// fallback if above is not valid
static std::false_type test(...);
public:
static constexpr bool value = std::is_same_v<decltype(test((T*)nullptr)), std::true_type>;
};
// generic print function
template<typename T>
void print(const T& value) {
// key point: use if constexpr to conditionally compile code
if constexpr (supports_ostream<T>::value) {
// branch 1: T supports stream operator
std::cout << "Printable via stream: " << value << std::endl;
} else {
// branch 2: T does not support stream operator
std::cout << "Type is not stream printable. Printing fallback message." << std::endl;
}
}
// create two test structs
struct Printable {
int x = 42;
};
// overload stream operator for Printable
std::ostream& operator<<(std::ostream& os, const Printable& p) {
return os << "Printable{" << p.x << "}";
}
// we do not overload stream operator for NotPrintable
struct NotPrintable {
std::string name = "SecretData";
};
int main() {
// call print with integral type
print(100);
Printable p;
print(p);
NotPrintable np;
print(np); // will not attempt to use stream operator
return 0;
}如果想要使用传统的 if 语句来实现类似的功能, 你会发现除了手动写几个 print 的重载版本之外, 几乎没有什么好办法,
一旦类型多起来代码就会很臃肿. 这是因为使用 if 的时候, 编译器必须保证所有路径上的代码是合法的(
因为编译期没人知道会跑到那条分支上), 这样就会导致 print 函数在处理 NotPrintable 类型时, 也会尝试实例化
std::cout << value, 导致编译错误. 而使用 if constexpr 则不会有这个问题, 对于那些不满足条件的分支, 编译器根本不会去看它们,
运行时也不可能跑到那些分支上. 这样就允许我们在模板中编写一些只有在特定条件下才有效的代码, 大大提高了模板编程的灵活性和可维护性.
if consteval 则是 C++23 才引入的一个新特性, 可以用来给函数指定两条执行路径: 编译期执行路径和运行时执行路径. 例如:
#include <iostream>
constexpr int factorial(int n) {
if consteval {
// compile-time path
return (n <= 1) ? 1 : n * factorial(n - 1);
} else {
// runtime path
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}
}
int main() {
const int n = 5;
constexpr int compile_time_result = factorial(n); // computed at compile time, no runtime overhead
std::cout << "Compile-time factorial(5): " << compile_time_result << std::endl;
int m;
std::cout << "Enter a number to compute factorial at runtime: ";
std::cin >> m;
int runtime_result = factorial(m); // computed at runtime
std::cout << "Runtime factorial(" << m << "): " << runtime_result << std::endl;
return 0;
}常见的比如编译期可以分析(或者预估)资源需求, 进行打表等操作, 而运行时则调用一些需要依赖动态输入的算法逻辑等. 这个
if consteval 实在是太新, 目前还没有太多资料可以参考, 有兴趣的读者可以查阅 C++23 的相关文档了解更多细节.
for 循环 (C++11)Range-based for 循环是 C++11 引入的一种简化的循环语法, 用来遍历容器和数组等可迭代对象. 它的语法如下:
for (declaration : expression) {
// loop body
}declaration 用来声明循环变量, 它会依次绑定到容器中的每个元素.expression 是一个可迭代对象, 例如标准容器, 数组, 字符串等.#include <vector>
std::vector<int> vec = {1, 2, 3, 4, 5};
for (int n : vec) {
std::cout << n << " "; // print each element
}通常情况下在遍历容器的时候我们不太关心元素的类型, 因此更常见的写法是使用 auto 关键字来自动推导循环变量的类型, 也就是
for (auto n : vec) 这样. 但需要记得, auto 会忽略掉引用和 const 修饰符, 因此如果你想要修改容器中的元素, 或者避免不必要的拷贝,
需要显式地使用引用类型, 例如:
for (auto &n : vec) {
n *= 2; // double each element
}使用 Range-based for 循环的前提是容器必须支持迭代器(Iterator)接口, 对于 STL 中的容器都支持. 这种写法显得简洁很多(
Java/C# 之类的语言早就有了).
结构化绑定(Structured Bindings)是 C++17 引入的一种语法糖, 用来将一个复合类型(例如结构体, 元组, 数组等)的成员拆解成多个独立的变量. 这样可以简化代码, 提高可读性. 结构化绑定的语法如下:
auto [var1, var2, ...] = expression;var1, var2, ... 是要绑定的变量列表, 它们会依次绑定到复合类型的成员.expression 是一个复合类型的表达式, 例如结构体对象, 元组对象, 数组等.例如:
#include <map>
std::map<std::string, int> myMap = {
{"one", 1},
{"two", 2},
{"three", 3}
};
for (const auto& [key, value] : myMap) {
std::cout << key << ": " << value << std::endl;
}在这个例子中, 我们使用结构化绑定将 std::map 中的键值对拆解成 key 和 value 两个变量, 使代码更加简洁和易读. 对于
std::pair 和 std::tuple 也有类似的操作. 对于那些不关心的成员, 可以使用下划线 _ 来忽略它们, 例如:
for (const auto& [key, _] : myMap) {
std::cout << "Key: " << key << std::endl;
}你是否在找? 🤓
t: tuple[int, str] = (42, "hello")
num, text = t
print(num)
print(text)列表初始化(List Initialization)是 C++11 引入的一种新的初始化语法, 用大括号 {} 来进行初始化. 它可以用来初始化基本类型,
结构体, 容器等各种类型. 列表初始化有以下几种形式:
int x{42}; // direct list initialization
std::vector<int> vec{1, 2, 3, 4, 5}; // direct list initialization of vectorint y = {42}; // copy list initialization
std::vector<int> vec2 = {1, 2, 3, 4, 5}; // copy list initialization of vectorstruct Point {
int x;
int y;
};
Point p{10, 20}; // aggregate initialization
int arr[3] = {1, 2, 3}; // aggregate initialization of arraystd::initializer_list 进行初始化, 例如:#include <initializer_list>
void foo(std::initializer_list<int> list) {
for (int n : list) {
std::cout << n << " ";
}
}
foo({1, 2, 3, 4, 5}); // initialize with an initializer list列表初始化有几个特点:
nullptr (C++11)nullptr 是 C++11 引入的一个关键字, 用来表示空指针. 在 C++11 之前, 通常使用 NULL 宏或者 0 来表示空指针,
但这两种方式都有一些缺点. 在一些平台上, NULL 会被定义为 0, 这可能导致隐式转换的问题, 例如在函数重载时可能会引起歧义.
nullptr 是一个类型安全的空指针常量, 它的类型是 std::nullptr_t, 可以隐式转换为任何指针类型.
std::optional (C++17)std::optional 是 C++17 引入的一个标准库模板类, 用来表示一个可能包含值也可能不包含值的对象. 它类似于其他语言中的
Option 或 Maybe 类型, 用来处理可能缺失的值. 例如:
#include <optional>
std::optional<int> findValue(bool found) {
if (found) {
return 42; // return a value
} else {
return std::nullopt; // return no value
}
}
int main() {
auto result = findValue(true);
if (result) {
std::cout << "Value found: " << *result << std::endl;
} else {
std::cout << "No value found." << std::endl;
}
return 0;
} 在这个例子中, findValue 函数返回一个 std::optional<int>, 表示可能包含一个整数值或者不包含值. 在调用该函数后,
我们可以通过检查 result 是否有值来决定如何处理结果. 如果有值, 可以使用解引用操作符 * 来获取实际的值.
std::optional 提供了一些有用的方法, 例如 has_value() 用来检查是否包含值, value() 用来获取值(如果没有值会抛出异常),
以及 value_or() 用来提供一个默认值等.
std::variant (C++17)std::variant 是 C++17 引入的一个标准库模板类, 用来表示一个类型安全的联合体(Union), 可以存储多种不同类型的值. 其实类似C中传统的
union, 但 std::variant 提供了类型安全的访问方式, 避免了传统联合体可能带来的类型错误问题. 例如:
#include <variant>
#include <iostream>
std::variant<int, double, std::string> createVariant(int type) {
switch (type) {
case 0:
return 42; // int
case 1:
return 3.14; // double
case 2:
return std::string("hello"); // string
default:
return {}; // default to empty variant
}
}在这个例子中, createVariant 函数返回一个 std::variant<int, double, std::string>, 可以存储整数, 双精度浮点数或者字符串.
根据传入的类型参数, 函数会返回不同类型的值.
要访问 std::variant 中存储的值, 可以使用 std::get 函数或者 std::visit 函数. 例如:
auto var = createVariant(1); // create a variant holding a double
try {
double d = std::get<double>(var); // get the double value
std::cout << "Double value: " << d << std::endl;
} catch (const std::bad_variant_access& e) {
std::cout << "Variant does not hold a double." << std::endl;
}
std::visit([](auto&& arg) {
std::cout << "Variant holds: " << arg << std::endl;
}, var);Type Traits 是 C++11 引入的一组模板类和函数, 用来在编译期查询和操作类型的属性. 它们定义在 <type_traits> 头文件中,
提供了丰富的类型信息和操作工具, 例如判断类型是否为某种特性, 获取类型的相关属性等. 在 C++17 之后提供了一些以 _v
结尾的变量模板, 用来简化类型特性的查询. 例如:
#include <type_traits>
template<typename T>
void checkType() {
if constexpr (std::is_integral_v<T>) {
std::cout << "T is an integral type." << std::endl;
} else if constexpr (std::is_floating_point_v<T>) {
std::cout << "T is a floating-point type." << std::endl;
} else {
std::cout << "T is neither integral nor floating-point." << std::endl;
}
}这个库提供了编译期类型查询的能力, 主要是为了模板编程服务的. 它本身非常依赖编译器开洞.
Concepts 是 C++20 引入的一种新的模板编程机制, 用来定义模板参数的约束条件. 用来解决大量模板编程一报错就不知道编译器又臭又长地在说什么的问题. 例如:
#include <concepts>
template<typename T>
concept Integral = std::is_integral_v<T>;
template<Integral T>
T add(T a, T b) {
return a + b;
}在这个例子中, 我们定义了一个名为 Integral 的概念, 用来约束模板参数 T 必须是整数类型. 然后在 add 函数模板中使用这个概念,
确保只有整数类型才能被传递给该函数. 如果传递了非整数类型, 编译器会报错, 并且错误信息会更加清晰和易懂.
concept 可以和 requires 关键字结合使用, 用来定义各种你想不到的约束方式. 例如:
template<typename T>
concept C = requires {
typename T::inner; // T must have a nested type named inner
typename S<T>; // require class template specialization
}
// compound requirements
template<typename T>
concept C2 = requires(T a) {
{*x} -> std::convertible_to<typename T::inner>;
{x + 1} -> std::same_as<int>;
}
template<class T, class U = T>
concept Swappable = requires(T&& t, U&& u) {
swap(std::forward<T>(t), std::forward<U>(u));
swap(std::forward<U>(u), std::forward<T>(t));
};甚至 requires 还能嵌套使用, 但这里就不展开讲了, 特性疑似有点太复杂了.
Ranges 是 C++20 引入的一个标准库模块, 用来简化对容器和序列的操作. 它提供了一组新的算法和视图(View), 允许以更直观和函数式的方式处理数据. Ranges 模块基于迭代器(Iterator)和范围(Range)的概念, 使得代码更加简洁和易读. 个人认为是 C++20 最有用的特性之一.
所谓范围, 指的是一个不持有数据的对象, 它提供了一种访问数据的方式, 或者说是对数据的一个“视图”, 例如:
#include <ranges>
std::vector<int> vec = {1, 2, 3, 4, 5};
// create a view that filters even numbers
auto even_view = vec | std::views::filter([](int n) { return n % 2 == 0; });
// iterate over the view
for (int n : even_view) {
std::cout << n << " "; // prints 2 4
}在这个例子中, 我们使用 Ranges 模块创建了一个过滤视图, 只包含偶数. 一个新语法是 | 操作符, 用来将容器和视图连接起来,
形成一个新的视图. 然后我们可以像遍历普通容器一样遍历这个视图. 视图可以串联起来, 形成复杂的数据处理管道, 例如:
auto processed_view = vec
| std::views::filter([](int n) { return n % 2 == 0; }) // filter even numbers
| std::views::transform([](int n) { return n * n; }); // square each number
for (int n : processed_view) {
std::cout << n << " "; // prints 4 16
}在这个例子中, 我们首先过滤出偶数, 然后对每个偶数进行平方操作.
Ranges 模块还提供了许多其他有用的视图和算法, 很多在 C++17 中的 STL 算法都被重新实现为 Ranges 版本, 例如
std::ranges::sort, std::ranges::find, std::ranges::copy, std::ranges::erase, std::ranges::reverse 等等.
这些算法可以直接作用于范围对象, 使得代码更加简洁和易读.
总之如果你想要高性能地遍历或者操作容器数据, Ranges 模块是一个非常强大的工具, 它几乎提供了你所有想要的功能, 并且语法简洁优雅, 非常值得学习和使用.
C++20 引入了 <format> 头文件, 提供了一种类型安全且高效的字符串格式化方式, 类似于 Python 的 f-string 或者 C# 的字符串插值.
它使用 std::format 函数来生成格式化的字符串, 语法如下:
#include <format>
std::string message = std::format("Hello, {}! You have {} new messages.", "Alice", 5);
std::cout << message << std::endl; // prints "Hello, Alice! You have 5 new messages."在这个例子中, std::format 函数接受一个格式字符串和一组参数, 并将参数插入到格式字符串中的占位符 {} 位置.
最有用的特性🤣, cout只能说要多难看有多难看, 输出内容一长是人类能看懂的东西吗? 还有高手, std::println甚至要等到 C++23
才引入.