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::movetemplate <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 &¶m) ;
如果传递的参数是左值(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 &¶m) { 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 ;
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 }; for (auto & elem : vec) { elem *= 2 ; } for (const auto & elem : vec) { std::cout << elem << " " ; } std::cout << std::endl; return 0 ; }
Output:
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_; };void PrintUniquePtr (std::unique_ptr<MyClass> ptr) { if (ptr) { ptr->Print (); } else { std::cout << "unique_ptr is null" << std::endl; } }std::unique_ptr<MyClass> CreateUniquePtr (const std::string& name) { return std::make_unique <MyClass>(name); }int main () { std::unique_ptr<MyClass> myPtr = CreateUniquePtr ("Kimi" ); myPtr->Print (); PrintUniquePtr (std::move (myPtr)); if (!myPtr) { std::cout << "myPtr is now null" << std::endl; } std::unique_ptr<MyClass> anotherPtr = CreateUniquePtr ("Moonshot AI" ); 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 : KimiName : KimiName : Kimi MyClass destroyed with name : Kimi myPtr is now null MyClass created with name : Moonshot AIName : 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; ~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_ptr = b; b->a_ptr = a; } return 0 ; }
Output:
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 ; if (auto ptr = std::any_cast <int >(&x); ptr != nullptr ) { std::cout << *ptr << std::endl; } x = "qwerty" ; if (auto ptr = std::any_cast <int >(&x); ptr != nullptr ) { std::cout << (*ptr) + 114514 << std::endl; } }int main () { foo (); }
Output:
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; } else { std::cout << "No value" << std::endl; } value = getValue (false ); if (value) { std::cout << "Value: " << *value << std::endl; } else { std::cout << "No value" << std::endl; } return 0 ; }
Output:
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> using MyVariant = std::variant<int , std::string>;void printVariant (const MyVariant& v) { std::visit ([](const auto & value) { std::cout << "Value: " << value << std::endl; }, v); }int main () { MyVariant v1 = 42 ; printVariant (v1); v1 = std::string ("Hello, World!" ); printVariant (v1); MyVariant v2 = "Another string" ; printVariant (v2); v2 = 123 ; printVariant (v2); return 0 ; }
Reference