
Effective Modern C++
文章平均质量分 77
《Effective Modern C++》学习笔记
Altair_Alpha_
这个作者很懒,什么都没留下…
展开
-
《Effective Modern C++》学习笔记 - Item 42: 考虑使用emplace代替插入
为容器添加元素时,我们自然想到的是使用 插入(insertion) 函数,包括 push_back,push_front,insert等。但是如果你追求极致的性能,那么这样做可能不是最优的。考虑下面的情况:class MyString {public: MyString() : str("default string.") {} MyString(const char* pc) : str(pc) { cout << "MyString: from st.原创 2022-04-05 23:00:22 · 1120 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 41: 考虑对一定会被拷贝,并且移动开销小的参数按值传递
有些函数的参数从设计上就是要被 “拷贝” 的,例如将参数拷贝加入类的容器中,具体可能是通过拷贝或移动的操作实现。为了效率,一般应该对左值参数做拷贝,右值参数做移动,例如下面的代码:class MyString {public: MyString() : str("default MyString.") {} MyString(const MyString& rhs) : str(rhs.get()) { cout << "MyString: co.原创 2022-04-03 22:29:50 · 964 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 40: 对于并发使用std::atomic,对于特殊内存使用volatile
std::atomic<T> 类型和 volatile 的功能完全不同,前者应该被用于实现并发的原子操作,而后者应该用于存储在特殊内存中的对象。std::atomic<T> 模板的实例化对象保证对该对象的操作对于所有线程都是原子的,表现如同受 mutex 保护一样(但是实现上一般会使用比 mutex 更底层高效的机器指令),例如:std::atomic<int> ai(0); // initialize ai to 0ai = 10; // ..原创 2022-04-03 13:46:41 · 841 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 39: 考虑使用void future实现单次事件通信
本节是一个小技巧,告诉我们可以使用 void 型的 future 实现线程间一次性的事件通信。如何实现两个线程间事件的通信?(例如,副线程需要等待一个数据才能继续计算,主线程将该数据准备完成后应通知副线程继续运行。以下代码中原书分别将其称为 react 和 detect 线程。)一种方法是使用 std::condition_variable:创建变量 cv,副线程调用 wait 阻塞等待信号;主线程准备完成后调用 notify_one 或 notify_all 发出信号,简单代码如下:..原创 2022-04-02 20:44:56 · 446 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 38: 注意future析构函数的不同行为
std::thread 和非延迟的 std::future 内部都对应着一个实际的线程,可以认为这些对象是我们对系统线程的 handle。从这个角度来看,std::thread 和 future 在析构函数中有不同的行为是比较奇怪的:如 Item 37 所述,销毁一个 joinable 的 std::thread 会导致程序终止;然而销毁 future 有时会表现地如隐式执行了 join,有时如隐式执行了 detach,有时二者皆非,但无论如何都不会导致程序终止。本节中,我们就对 future 的..原创 2022-04-01 00:04:32 · 1392 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 37: 保证std::thread在所有运行路径上最终unjoinable
每个 std::thread 对象随时处于两种状态之一:joinable 或 unjoinable。处于 joinable 状态意味着 std::thread 对象必须对其内部的线程资源负责,该线程的状态可以是等待执行、正在执行或已经完成执行。而 unjoinbale 状态正相反,具体包括几种情况:默认构造的 std::thread。它们没有要执行的函数,因此也不会真正持有一个线程。已经被用于移动构造其它对象的 std::thread。移动操作将其内部持有线程的控制权交给了其它对象,因此它不再负责管.原创 2022-03-29 23:14:02 · 1059 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 36: 如果异步性是必要的,指明std::launch::async
如上节所述,使用 std::async 相比手动创建线程有若干优点。但是,调用 std::async 不保证传入的函数(或 callable object)一定是在其它线程异步运行的。std::async 实际是遵循 运行策略(launch policy) 执行的,标准库在 std::launch 枚举类中声明了两种策略(设待执行的函数名为 f ):std::launch::async 运行策略表示 f 必须 被异步地(即在另一个线程上)执行。std::launch::deferred 运行..原创 2022-03-26 23:16:06 · 1526 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 35: 倾向于基于任务而非基于线程的编程方式
本节开始我们进入全书第7章:并发 API本节太长不看版:使用基于线程的编程方式,你需要自己管理线程的生命周期,考虑线程性能相关的多种问题,而且无法直接获取返回值;而使用基于任务的方式,这些问题全部交由标准库实现解决,而且可以通过 get() 获取返回值。C++11 中,想要异步运行一个函数 doAsyncWork() 有两种基本选择:基于线程(thread-based)的方法:创建一个 std::thread,并让它运行 doAsyncWork。#include <thread&g..原创 2022-03-25 22:13:55 · 840 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 34: 倾向于使用lambda函数而非std::bind
太长不看版:std::bind 是2005年TR1提案引入的特性,相比C++98的 std::bind1st 和 std::bind2nd 是一个大进步;但到了C++11时代,lambda 函数几乎可以代替它,但仍剩余几种特定情况适合使用 std::bind。C++14消灭了这些情况,现在 lambda 函数已经完全是 std::bind 的上位替代。本节通过几个例子来展示 lambda 相比std::bind的若干优势。我们假设你已经熟悉 std::bind 的基本使用。lambda 的第..原创 2022-03-24 19:32:18 · 1004 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 33: 对auto&&参数使用decltype以转发它们
C++14 为我们带来了泛型 lambda(generic lambda),可以在参数声明中使用 auto。这个特性的实现原理也不难想到:将 lambda 生成类的 operator() 变为一个模板函数即可。例如,下面的 lambda 函数在编译中产生的类可能形如:auto f = [](auto x){ return func(normalize(x)); };↓class SomeCompilerGeneratedClassName {public: template<typena.原创 2022-03-15 20:50:51 · 1234 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 32: 使用初始化捕获将对象移动至lambda函数中
C++11 中 lambda 函数捕获参数的语义只有两种:按值捕获 [=] 和按引用捕获 [&],而且只能声明变量名,不支持表达式。这导致了一个问题:无法使用移动构造的参数。这对于拷贝成本高而移动成本低的对象(如STL中的大部分容器)以及只能移动构造的对象(如 std::unique_ptr 和 std::future)是很不友好的。C++14 对此做了改进,引入了一种新的捕获机制:初始化捕获(init capture),或者叫广义 lambda 捕获(generalized lambda..原创 2022-03-15 19:55:56 · 1075 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 30: 了解完美转发失效的场景
完美转发(perfect forwarding),是指一个函数将其参数完整传递给另一个函数,使得第二个函数接受的对象与第一个函数完全相同,包括其类型、是左值还是右值、是否是 const 或 volatile 的属性。因此我们这里讨论的只有万能引用参数,只有它们能携带这些信息。以下面这个函数为例:template<typename T>void fwd(T&& param) // accept any argument{ f(std::forward<T&g.原创 2022-03-12 23:00:22 · 1088 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 29: 移动操作的“坑点”:它们可能不存在,开销不小或不会被调用
移动语义无疑是C++11的一个重要特性,然而人们容易对其期待过高。本节就来介绍一些移动操作并不能带来性能提升的情景,目的是使我们对移动操作的作用范围有更加理性的认识。以下所述的移动操作均为移动构造函数和移动赋值运算符两项的集合。首先,很多类型不支持移动语义。虽然C++11的STL实现已经针对移动操作做了改进,但你正在使用的其它符合C++98标准的库可能并非如此。的确,编译器会自动为类生成移动操作,但根据 Item 17,只有当该类没有声明拷贝操作、(另一种)移动操作和析构函数时才会如此。对于没有显..原创 2022-03-05 18:15:06 · 658 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 28: 理解引用折叠(reference collapsing)
本节中,我们来讨论“万能引用”这种抽象的本质,以及 std::forward 的实现机理。Item 23和24中说明了当使用万能引用参数 T&& 时,入参的类型信息会被记录在 T 中。规则实际上很简单:当入参为左值时,T(注意,不是 T&&)被推导为左值引用;当入参为右值时,T 被推导为非引用(注意这里的不对称性)。以一个例子来说明:template<typename T>void func(T&& param);Widget..原创 2022-03-01 15:33:52 · 921 阅读 · 2 评论 -
《Effective Modern C++》学习笔记 - Item 27: 了解对万能引用参数进行重载的替代方案
本节是 Item 26 的延续。上一节中我们已经了解了为什么对万能引用参数进行重载是种糟糕的做法,这一节就来讨论其替代做法。方法一:放弃使用重载最简单的方法。例如,上一节中的 logAndAdd 函数可以拆为两个 logAndAddName 和 logAndAddIdx。但这种方法对构造函数的情景不适用,因为构造函数的名称是固定的。再说了,重载多好用,谁又想放弃呢?方法二:按 const T& 传递也就是回到上一节的最初版本,放弃使用万能引用。如之前讨论,这样的代码效率不是..原创 2022-02-21 23:11:13 · 658 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 26: 避免对万能引用参数进行重载
假设有下面这样一个函数,它将一个字符串参数 name 加入到全局数据集合 names 中,同时记录当前时间:std::multiset<std::string> names; // global data structurevoid logAndAdd(const std::string& name){ auto now = // get current time std::chrono::system_clock::now(); log(now, .原创 2022-02-19 16:52:18 · 881 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 25: 对右值引用参数使用std::move,对万能引用参数使用std::forward
在函数中,对右值引用的参数使用 std::move,对万能引用的参数使用 std::forward,不能用反:对右值引用使用 std::forward<T>() 需要额外写一遍类型 T,代码冗余容易出错;对万能引用使用 std::move 更糟糕,因为万能引用参数可以绑定到左值引用,使用 std::move 可能会错误地破坏传入的左值参数(准确来说,是错误地将其强制转型为右值,导致后续使用时可能被破坏)。在一个函数中如果要多次使用一个右值/万能引用对象,仅在最后一次对其用 st..原创 2022-02-19 12:20:11 · 657 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 24: 区分万能引用和右值引用
T&& 有两种含义:右值引用,只能绑定到右值上。万能引用(作者称为 “universal references” ),可以绑定到任何类型的变量上,包括左值或右值,const 或 non-const,volatile 或 non-volatile,以及以上的任何组合。万能引用出现在两个情景中:模板函数的参数, 形式为:template<typename T>void f(T&& param); // param is a universa..原创 2022-02-14 22:55:08 · 593 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 23: 理解 std::move 和 std::forward
std::move 不会移动任何东西,std::forward 不会传递任何东西。二者在运行时什么都不会做。std::move 和 std::forward 只是执行类型转换(cast)的函数(模板)。std::move 无条件地将参数转换为右值,而 std::forward 只有当满足某个条件时才做如此转换。就这样。看看 std::move 的实现:// GCC版本,MSVC与之区别仅在于将 remove_reference<_Tp>::type换成了remove_reference.原创 2022-01-28 12:03:58 · 1276 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Chapter 5. 右值引用,移动语义和完美转发
第五章第一次学习时,移动语义和完美转发的意思看起来很直白:移动语义使编译器能把昂贵的拷贝操作换为移动操作。类似于拷贝构造/赋值函数,你可以通过移动构造/赋值函数来控制移动的语义。移动语义还允许你创建仅移动(不能复制)的类型,如 std::unique_ptr,std::future 和 std::thread。完美转发允许函数模板接受任意(左值/右值)参数,并将它们原样传递给其它的函数。右值引用是使以上二者可能实现的低层语言机制然而随着对这些特性的掌握变深,你会发现最初的印象只是冰原创 2022-01-24 17:38:33 · 530 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 22: 使用 Pimpl 手法时,在实现文件中定义特殊成员函数
Pimpl (pointer-to-implementation) 是一种常用的类设计手法,可以实现:(1)只暴露函数调用接口,隐藏实现细节;(2)避免修改实现时导致调用者的代码必须重新编译,提高构建速度。实现 Pimpl 的方法正如其名:头文件(.h)中,去除类中所有私有成员;声明(而不定义)一个 impl 类型,为类添加一个 private 的 impl 指针。实现文件中,定义 impl 类型,包含所有私有成员,其它函数实现通过 impl 指针访问它们;构造和析构函数实现中,new 和 delete .原创 2022-01-17 11:21:59 · 806 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 21: 倾向于使用 std::make_unique 和 std::make_shared 而非直接使用 new
注:本节所述内容对于C++11、14和C++17标准来说有一些不同。原书基于前者讲解,笔者会标注出C++17的变化。std::make_unique 和 std::make_shared 函数正如其名称所示,用于创造一个 std::unique_ptr 或 std::shared_ptr。它们接收任意多个参数,第一个参数作为管理的对象类型,并将剩余所有参数完美转发给对象类型的构造函数。与 std::make_shared 类似的还有一个 std::allocate_shared,区别在于其第一个参..原创 2022-01-16 22:15:20 · 464 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 20: 使用 std::weak_ptr 管理可能悬空的类似 std::shared_ptr 的指针
有时我们需要一种行为类似 std::shared_ptr,但不影响目标对象引用计数的智能指针。这种指针需要处理一种对 std::shared_ptr 来说不存在的问题:其指向的对象可能已被销毁。即本节主题 std::weak_ptr。std::weak_ptr 不能通过 operator * 直接解引用。若想检查...原创 2022-01-16 18:48:42 · 307 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 19: 使用 std::shared_ptr 管理共享性资源(附MSVC源码解析)
很多编程语言中垃圾回收(GC)的确是非常便利的特性,但它们执行的时间往往不可预期;而C++98的全手动资源管理又显得过于“原始”了一点。将能够自动进行操作(GC)以及操作的时机可预期(析构函数)这两个优势结合在一起的,就是C++11的 std::shared_ptr。任一个 std::shared_ptr 都不拥有它管理的对象;但它们共同确保对象不再被需要时会被销毁。其中主要的实现机制是 引用计数(reference count):一个资源被多少个 std::shared_ptr 指向的计数。st..原创 2021-12-28 11:14:30 · 1129 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 18: 使用 std::unique_ptr 管理独占性资源(附MSVC源码解析)
std::unique_ptr 占用空间与裸指针相同,大多数操作(包括解引用)的方法也相同。它表现的是 独占性拥有(exclusive ownership) 的语义,这意味着它不能被拷贝,是 move-only 类型;当其自身被销毁时,总会销毁其拥有的资源。而对象的销毁在作用域结束(即右大括号出现)时自动通过调用析构函数进行,此时 std::unique_ptr 默认对内部的裸指针进行 delete,因此你无需像裸指针一样操心什么时候要手动销毁。(注:笔者尽己所能对MSVC的 std::unique_p.原创 2021-12-27 13:27:59 · 1013 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Chapter 4. 智能指针
让我们列举一下为什么很难爱上一个裸指针(raw pointers):其声明不能指示其指向的是一个单独对象还是数组。其声明不能告诉你应否在使用完毕时销毁它,换言之你不知道它是不是(独占地)拥有着它指向的东西。如果你决心销毁它,你没有办法知道如何做到——你是应该用 delete,还是有一套其它的销毁机制(或许是调用一个专门的函数)?如果你确定 delete 是合适的方法,原因一告诉我们你无法确定应该用单个对象销毁的 delete 形式,还是数组销毁的 delete [] 形式。如果用错了,结果就是.原创 2021-12-26 15:49:55 · 752 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 17: 理解特殊成员函数的生成
特殊成员函数(special member functions) 指C++会自动生成的函数。C++98中有4个这样的函数:默认构造函数(default constructor),析构函数(destructor),复制构造函数(copy constructor)和复制赋值运算符(copy assignment operator)。只有当需要(即类中没有定义但有调用)时,它们才会被生成。其中默认构造函数只有当类没有声明任何构造函数时才会被生成。生成的函数默认是 public 且 inline 的。它们..原创 2021-12-25 11:35:27 · 825 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 16: 确保 const 成员函数线程安全
本节作者想表达的 point 是,由于 const 成员函数代表的一般是只读操作,客户端的调用代码有理由在多线程调用中不考虑同步问题。但实际上有时在 const 函数中也需要修改数据成员,此时函数的线程安全性就应该由你来保证。当然,作者也点明了如果你能确保函数是100%不会涉及多线程调用场景,的确没必要考虑这点,然而实际上完全非多线程的应用场景是越来越少的。假设有一个 Point 类,其中有一个返回到原点距离的函数 distanceFromOrigin,显然它可以是 const 的。现在如果我们有一个需.原创 2021-12-24 15:39:05 · 960 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 15: 尽可能使用 constexpr
constexpr 关键字的概念丰富,作用于对象和函数时意义不同。作用于对象,constexpr 描述的对象是常量,且值在编译期已知。 int sz; // 未初始化的int constexpr auto arraySize1 = sz; // error! sz的值在编译期未知 const auto arraySize2 = sz; // ok. arraySize2是sz的const copy。MSVS过编译但链接报错,GCC可以运行且..原创 2021-12-23 23:09:39 · 729 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 13: 使用迭代器时尽可能使用 const_iterator
本节的核心理念是 “尽可能使用 const 标识符” 这一大原则用在迭代器 iterator 上,idea本身没有什么值得争论的。接下来主要聊一聊C++98时很多情况为什么不用 const iterator,C++11/14做了什么改进,以及现在如何使用。C++98中,假设有一个需求:在 std::vector<int> 中查找一个值 targetVal,然后在其位置(之后)插入另一个值 inserVal,如果找不到则插入在最后。很自然的可以写出如下代码:std::vector&..原创 2021-12-22 23:00:47 · 627 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 12: 使用 override 关键字声明重写函数
C++11中引入了 override 关键字,现在当你在派生类中重写基类函数时应添加这一关键字。熟悉 Java 或 C# 的同学应该对此不陌生。本节核心内容就是这句话,接下来关于成员函数性质的讨论可能有些偏题,不过笔者认为其中的信息是非常有价值的。函数重载(overload)和函数重写(override),二者名称相似但完全无关,这里不再赘述。以下函数均不会重写,然而也不会有任何报错(甚至连 warning 都没有)。这些都是我们可能无意识会犯的错误。class Base{public: .原创 2021-12-22 13:44:55 · 408 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 11: 倾向于使用delete而不是private+只声明不定义来屏蔽函数
本条款的原英文标题是 “Prefer deleted functions to private undefined ones”,不太好理解,笔者根据内容做了个转述。想表达的是:当你需要屏蔽掉某个特定的函数时,使用C++11的 delete 特性而不是像C++98中将函数声明为 private 且不给定义。如果你在给其他开发者提供函数或库,有时你会希望阻止他们调用某些函数,但这些函数又被C++自动生成了。该问题主要集中于复制构造函数和赋值运算符上(先不考虑C++11新引入的移动构造函数)。例如,iostr.原创 2021-12-21 20:43:11 · 680 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 10: 倾向于使用有界的枚举(scoped enum)而不是无界的枚举(unscoped enum)
一般在大括号内声明的名称(name,或者严谨点说标识符)的可见范围是限制在大括号内的。然而C++98中的 enum 不同:它内部定义的名称会泄露到定义 enum 自身的区域中。因此也得名 unscoped enum:enum Color { black, white, red };auto white = false; // VS报错: main::white重定义C++11版本的 scoped enums 通过 enum class 定义,不会导致名称的泄露:enum class Col.原创 2021-12-21 13:12:13 · 471 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 9: 倾向于使用别名声明(alias declaration)而不是 typedef
alias declaration 和 typedef 做的事情完全相同,并且有充分理由使用前者替代后者。为了引入,作者开了个玩笑:如果你要用一个类型std::unique_ptr<std::unordered_map<std::string, std::string>>,你绝对不想把它写两次。想想都觉得要增加得腕管综合征(俗称:鼠标手)的风险。C++11中 alias declaration 的语法为:using UPtrMapSS = std::unique_ptr&.原创 2021-12-20 20:59:31 · 658 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 8: 倾向于使用 nullptr 而不是 0 或 NULL
本Item内容较简单,养成良好习惯多用 nullptr 少用 0 和 NULL 就好了。实际上,我认为可以说前者是从语言层面填补了C++98时代没有一个明确的空指针类型的空缺,因此在条件允许的情况下使用前者是完全优于后者的。以下是作者给出的几个更详细的原因。0 的本质是 int,只有当不得已时才会被转化为指针。NULL 同理(注:NULL 是一个宏定义,查看源码MSVC和GCC根据平台将其定义为了0 或 0LL。作为参考,其在C语言中的定义一般为(void *)0)。这里的重点是二者都不具有指针类..原创 2021-12-20 00:06:41 · 542 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 7: 创建对象时区分使用小括号 () 和大括号 {}
C++11中有四种初始化方法:int x(0); // 小括号 (parentheses) 初始化int y = 0; // 等号初始化int z{ 0 }; // 大括号 (braces) 初始化int z = { 0 }; // 1、3结合其中方法4基本被视为与方法3相同,以下讨论中忽略。C++98中的老规矩:声明时不加任何括号等号,调用默认构造函数(default constructor);声明时用等号或括号,调用复制构造函数(copy constructor);非声明.原创 2021-12-18 22:07:15 · 612 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Chapter 3. 走向现代C++
《Effective Modern C++》学习笔记系列第三章索引原创 2021-12-18 20:30:27 · 568 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 6: 当auto推导出非预期类型时,使用“显式类型初始化器”
笔者注:本Item是对上一Item的补充,介绍了 auto 不如预期的可能情况以及解决方法(不是直接对变量用显式类型标识符哦)对于 std::vector,如果存储的数据类型是 bool,那么通过 operator[] 索引取到的数据类型不是按照一般规则的 bool& ,而是 std::vector<bool>::reference(只有 bool 这一种例外)。如果使用 auto 推导,就会得到错误的变量类型。这是因为 std::vector<bool> 是逐位.原创 2021-12-18 20:18:25 · 577 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Item 5: 倾向于使用auto而不是显式类型声明
先来定义一个简单的局部变量:int x; // 糟糕,忘记初始化了。x也许会被初始化为0也许不会,取决于上下文。别在意。再来定义一个用迭代器(Iterator)解引用初始化的局部变量:template<typename It>void dwim(It b, It e){ while (b != e) { // 真的假的?声明一个变量这么麻烦? typename std::iterator_traits<It>::value.原创 2021-12-16 12:47:18 · 421 阅读 · 0 评论 -
《Effective Modern C++》学习笔记 - Chapter 2. auto
《Effective Modern C++》学习笔记系列第二章索引原创 2021-12-16 10:18:46 · 264 阅读 · 0 评论