Modern Cpp

Before:上机课偶遇modern cpp,拼劲全力无法战胜

Modern Cpp

Overview!

关于C++的刻板印象是什么?😋
笔者在学习了Java(以及相当烂的py)后,深深地感受到了C++语法规则以及一些奇奇怪怪的规定的复杂😇😇。总结一下C++:

  • Old, out-dated, less-frequently used
  • Unsafe (特别对,救命啊)
  • Hard to use
  • Various Complication Issues

Anyway,we still need to learn Modern Cpp.
We will cover:

  • std::move and value types
  • Type inference and std::forward
  • auto inference
  • Syntax sugar
  • Smart pointers
  • Safety

Value Types

左值 右值

  • 左值表示了一个占据内存中某个可识别的位置(也就是一个地址)的对象。可以用&
  • 右值则相反,是一个临时对象,不可以用&

我们知道有2种赋值方式:拷贝赋值和移动赋值。对于a = xxx:

  • 拷贝赋值函数:xxx为左值
  • 移动赋值函数:xxx为右值

Move for lvalue?std::move!

一个Common sense是移动move比拷贝要快,如果我们想移动一个左值呢

我们可以用std::move for this.
std::move可以让编译器认为某个左值是一个右值,进行了所有权的转移,使用了std::move后的对象不可再使用。

const变量不可以使用移动语义。
Notice:以下地方不可用std::move

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void Print(const std::string &s);
std::string Concat(const std::string &p, std::string q) {
∕∕ Move from a const var
std::string tmp = std::move(p);

∕∕ Move from a rvalue
std::string tmp2 = std::move(p + q);
std::string ret = p + q;

∕∕ Move to a const value reference
Print(std::move(p + q));

∕∕ Move the return value or use after move
return std::move(ret);
}

std::move的实现并不同于想象中的复杂类型转换,实际上它只用了static_cast

1
2
3
4
5
∕∕ A sample implementation of std::move
template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

Universal Reference 通用引用

1
2
template<typename T>
void func(T &&param);

如果传递的参数是左值(lvalue),则 T 会被推断为该类型的引用。如果传递的参数是右值(rvalue),则 T 会被推断为该类型的值类型。
所以如果param是左值,T的类型会是int &,那么param的类型会是int& &&,C++会将其折叠为int&。(引用折叠

如果param是右值,T的类型会是int。那么param的类型会是int&&。
Attach a test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <utility>

template <typename T>
void func(T &&param) {
if constexpr (std::is_lvalue_reference_v<T>) {
std::cout << "Left value passed. Type of param is " << typeid(T).name() << std::endl;
} else {
std::cout << "Right value passed. Type of param is " << typeid(T).name() << std::endl;
}
}

int main() {
int a = 10;
func(a); // 左值传递

int b = 20;
func(std::move(b)); // 右值传递

int &c = a;
func(c);
func(100);

return 0;
}

std::forward 完美转发

笔者在stackoverflow上找了一些观点
std::forward is really just syntactic sugar over static_cast<T&&>.Nicol Bolas CommentedDec 15, 2011 at 21:19

The concepts that seems to be lacking is that type (for instance int) is not the same thing as “value category” (an int can be sometimes a lvalue if you use a variable int a, sometimes rvalue if you return it from a function int fun()). When you look at a parameter thing&& x its type is an rvalue reference, however, the variable named x also has a value category: it’s an lvalue. std::forward<> will make sure to convert the “value category” x to match its type. It makes sure a thing& x is passed as a value category lvalue, and thing&& x passed as an rvalue. arkan CommentedOct 1, 2022 at 14:19

std::forward 的作用是根据模板参数的类型,将参数转发为左值或右值。

1
2
template <typename T>
constexpr T&& forward(std::remove_reference_t<T>& t) noexcept;//移除T的引用类型

Attach a test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
void process(int& x) {
std::cout << "lvalue" << std::endl;
}
void process(int&& x) {
std::cout << "rvalue" << std::endl;
}

template <typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
int main() {
int a = 10;
wrapper(a); // 输出 "lvalue"
wrapper(20); // 输出 "rvalue"
}

Auto Reference 自动类型推断

  • auto可推断变量类型
  • auto& 可推断引用类型
  • const auto& 可推断常量引用

Attach a test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <vector>

int main() {
// 创建一个包含整数的向量
std::vector<int> vec = {1, 2, 3, 4, 5};

// 使用 auto& 遍历向量
for (auto& elem : vec) {
// 直接修改容器中的元素
elem *= 2;
}

// 输出修改后的向量
for (const auto& elem : vec) {
std::cout << elem << " ";
}
std::cout << std::endl;

return 0;
}

Output:

1
2 4 6 8 10

Important Part: Smart Pointers!智能指针

unique_ptr

一个Move Only的智能指针,只可以拥有一个拥有者。
Attach an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <iostream>
#include <memory>
#include <string>

// 定义一个简单的类
class MyClass {
public:
MyClass(const std::string& name) : name_(name) {
std::cout << "MyClass created with name: " << name_ << std::endl;
}
~MyClass() {
std::cout << "MyClass destroyed with name: " << name_ << std::endl;
}

void Print() const {
std::cout << "Name: " << name_ << std::endl;
}

private:
std::string name_;
};

// 函数接受一个 unique_ptr 并打印内容
void PrintUniquePtr(std::unique_ptr<MyClass> ptr) {
if (ptr) {
ptr->Print();
} else {
std::cout << "unique_ptr is null" << std::endl;
}
}

// 函数返回一个 unique_ptr
std::unique_ptr<MyClass> CreateUniquePtr(const std::string& name) {
return std::make_unique<MyClass>(name);
}

int main() {
// 创建一个 unique_ptr
std::unique_ptr<MyClass> myPtr = CreateUniquePtr("Kimi");

// 使用 unique_ptr
myPtr->Print();

// 将 unique_ptr 传递给另一个函数
PrintUniquePtr(std::move(myPtr));

// myPtr 已经被移动,现在是空的
if (!myPtr) {
std::cout << "myPtr is now null" << std::endl;
}

// 创建另一个 unique_ptr
std::unique_ptr<MyClass> anotherPtr = CreateUniquePtr("Moonshot AI");

// 使用 unique_ptr 的 reset 方法
anotherPtr->Print();
anotherPtr.reset(); // 手动释放资源
if (!anotherPtr) {
std::cout << "anotherPtr is now null" << std::endl;
}

return 0;
}

Output:

1
2
3
4
5
6
7
8
9
MyClass created with name: Kimi
Name: Kimi
Name: Kimi
MyClass destroyed with name: Kimi
myPtr is now null
MyClass created with name: Moonshot AI
Name: Moonshot AI
MyClass destroyed with name: Moonshot AI
anotherPtr is now null

shared_ptr

基本特性:

  • 可以有多个所有者,是可拷贝的,但需要注意循环引用问题。
  • 只有当所有拥有者都释放它时才会销毁(通过引用计数实现)。

shared_ptr的特殊用途
解决类之间的循环引用:
如果两个类相互包含对方的对象,会导致编译错误,因为C++需要在编译时知道类的大小。
使用shared_ptr可以解决这个问题,因为shared_ptr只需要类的声明而不需要定义就可以使用。

Attach an example:

1
2
3
4
5
6
7
8
9
10
11
class B;
class A {
// 其他方法和成员
private:
std::shared_ptr<B> b;
};
class B {
// 其他方法和成员
private:
std::shared_ptr<A> a;
};

weak_ptr

在使用智能指针shared_ptr的时候,可能会存在循环引用的问题,例如智能指针a指向智能指针b,智能指针b指向智能指针a。此时两个智能指针的引用计数都不为1,此时存在内存泄露,两个指针指向的内存不会被释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <memory>
using namespace std;
class Node {
public:
std::shared_ptr<Node> next;

Node() { std::cout << "Node created\n"; }

~Node() { std::cout << "Node destroyed\n"; }
};


void test_bug_for_shared_ptr() {
std::shared_ptr<Node> ptr1 = std::make_shared<Node>();
std::shared_ptr<Node> ptr2 = std::make_shared<Node>();
ptr1->next = ptr2;
ptr2->next = ptr1;
}

int main() {
test_bug_for_shared_ptr();
}

Output:

1
2
Node created
Node created

weak_ptr 是 C++11 引入的一种智能指针,用于解决 shared_ptr 的循环引用问题。它允许一个对象安全地引用另一个对象,但不会增加引用计数。
Attach a test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <memory>
#include <vector>

// 定义一个简单的类
class B; // 前置声明

class A {
public:
std::shared_ptr<B> b_ptr;
~A() {
std::cout << "A destroyed" << std::endl;
}
};

class B {
public:
std::weak_ptr<A> a_ptr; // 使用 weak_ptr 避免循环引用
~B() {
std::cout << "B destroyed" << std::endl;
}
};

int main() {
// 创建两个对象,避免循环引用
{
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();

// A 持有 B 的 shared_ptr
a->b_ptr = b;

// B 持有 A 的 weak_ptr
b->a_ptr = a;

// 当作用域结束时,a 和 b 会被正确销毁
} // 这里不会导致内存泄漏

return 0;
}

Output:

1
2
A destroyed
B destroyed

std::any

功能:
允许在C++中以类似弱类型语言的方式使用变量。
可以存储任何类型的数据,并在需要时进行类型转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <any>
void foo() {
std::any x = 114514; // 1
if (auto ptr = std::any_cast<int>(&x); ptr != nullptr) { // 2
std::cout << *ptr << std::endl; // 3
}
x = "qwerty"; // 4
if (auto ptr = std::any_cast<int>(&x); ptr != nullptr) { // 5
std::cout << (*ptr) + 114514 << std::endl; // 6
}
}

int main() {
foo();
}

Output:

1
2
114514

auto ptr = std::any_cast<int>(&x)会判断ptr是否可以转化为一个int*类型的指针,如果可以就做取地址,如果不可以就变成nullptr

std::optional and std::variant

std::optional

  • 可以存储类型T的值或者什么也不存储(类似于指针,但更安全)。
  • 用于表示可选值,避免使用裸指针带来的空指针问题。

Attach a test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <optional>

std::optional<int> getValue(bool returnValue) {
if (returnValue) {
return 42; // 返回一个值
}
return std::nullopt; // 返回空
}

int main() {
auto value = getValue(true);
if (value) {
std::cout << "Value: " << *value << std::endl; // 输出 42
} else {
std::cout << "No value" << std::endl;
}

value = getValue(false);
if (value) {
std::cout << "Value: " << *value << std::endl; // 输出 42
} else {
std::cout << "No value" << std::endl;
}
return 0;
}

Output:

1
2
Value: 42
No value

std::variant

  • 可以存储多种类型的数据(类似于union,但使用起来更方便)。
  • 用于存储不同类型的数据,并且可以在运行时安全地访问和转换。

Attach an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <variant>
#include <string>

// 定义一个 std::variant,可以存储 int 或 std::string
using MyVariant = std::variant<int, std::string>;

void printVariant(const MyVariant& v) {
// 使用 std::visit 访问 std::variant
std::visit([](const auto& value) {
std::cout << "Value: " << value << std::endl;
}, v);
}

int main() {
// 创建一个 std::variant,初始值为 int
MyVariant v1 = 42;
printVariant(v1); // 输出: Value: 42

// 修改 std::variant 的值为 std::string
v1 = std::string("Hello, World!");
printVariant(v1); // 输出: Value: Hello, World!

// 创建一个 std::variant,初始值为 std::string
MyVariant v2 = "Another string";
printVariant(v2); // 输出: Value: Another string

// 修改 std::variant 的值为 int
v2 = 123;
printVariant(v2); // 输出: Value: 123

return 0;
}

Reference


Modern Cpp
http://example.com/2025/04/14/Modern-Cpp/
Author
Yihan Zhu
Posted on
April 14, 2025
Updated on
April 17, 2025
Licensed under