好奇的探索者,理性的思考者,踏实的行动者。
Table of Contents:
c++17
std::invoke std::apply
* apply
Invoke the Callable object f with a tuple of arguments.
c++20
模组(Modules)
协程(Coroutines)
标准库 Concepts 的概念
范围(range)
constexpr支持:new/ delete、dynamic_cast、try/ catch、虚拟
constexpr 向量和字符串
计时:日历、时区支持
std::format
std::span
std::jthread
在 C/C++ 里,所有的函数都是全局的,没有生存周期的概念(static、名字空间的作用很弱,只是简单限制了应用范围,避免名字冲突)
而且函数也都是平级的,不能在函数里再定义函数,也就是不允许定义嵌套函数、函数套函数
所以,在面向过程编程范式里,函数和变量虽然是程序里最关键的两个组成部分,但却因为没有值、没有作用域而不能一致地处理。函数只能是函数,变量只能是变量。
因为 lambda 表达式是一个变量,所以,我们就可以“按需分配”,随时随地在调用点“就地”定义函数,限制它的作用域和生命周期,实现函数的局部化。
而且,因为 lambda 表达式和变量一样是“一等公民”,用起来也就更灵活自由,能对它做各种运算,生成新的函数。这就像是数学里的复合函数那样,把多个简单功能的小lambda 表达式组合,变成一个复杂的大 lambda 表达式。
C++ 里的 lambda 表达式除了可以像普通函数那样被调用,还有一个普通函数所不具备的特殊本领,就是可以捕获外部变量,在内部的代码里直接操作。
看到这里,如果你用过 JavaScript,那么一定会有种眼熟的感觉。没错,lambda 表达式就是在其他语言中大名鼎鼎的“闭包”(closure),这让它真正超越了函数和函数对象。不过在c++中闭包的作用不大,因为有比较强的面向对象编程。
在 lambda 表达式赋值的时需用auto关键字,在 C++ 里,每个 lambda 表达式都会有一个独特的类型,而这个类型只有编译器才知道,我们是无法直接写出来的,所以必须用 auto。
可以使用 auto 自动推导类型存储 lambda 表达式,但 C++ 鼓励尽量就地匿名使用,缩小作用域;
在 C++14 里,lambda 表达式又多了一项新本领,可以实现“泛型化”,相当于简化了的模板函数,具体语法还是利用了“多才多艺”的 auto
。
// 参数使用auto声明,泛型化
auto f = [](const auto& x)
{
return x + x;
};3) << endl; // 参数类型是int
cout << f(0.618) << endl; // 参数类型是double
cout << f("matrix";
string str = // 参数类型是string cout << f(str) << endl;
实现方式为函数对象,一个lambda就是一个可调用对象。
底层上,lambda 表达式被编译器转化为一个临时的、匿名的类类型对象,这个对象重载了 operator() 运算符,从而使得这个对象可以像函数一样被调用。这个对象通常称为闭包(Closure),它包含了 lambda 表达式的捕获变量、参数和函数体。
class LambdaClosure {
public:
int captured_value, const int& captured_reference)
LambdaClosure(
: captured_value(captured_value), captured_reference(captured_reference) {}
int operator()(int x, int y) const {
return x + y + captured_value + captured_reference;
}
private:
int captured_value;
const int& captured_reference;
};
int main() {
int captured_value = 10;
int captured_reference = 15;
auto lambda_captured = [captured_value, &captured_reference](int x, int y) -> int {
return x + y + captured_value + captured_reference;
};
int result_lambda = lambda_captured(3, 5); // 调用 lambda 表达式
int result_closure = LambdaClosure(captured_value, captured_reference)(3, 5); // 调用转化后的类的 operator() 方法
std::cout << "Result from lambda: " << result_lambda << std::endl;
std::cout << "Result from closure: " << result_closure << std::endl;
return 0;
}
主要作用:
1.将函数变成first class。
2.做回调函数。
# [capture](parameters)->return-type{body}
[外部变量访问方式说明符(=|&)] (参数表) -> 返回值类型
{
语句块
}
[](int x, int y) -> int { int z = x + y; return z + x; }// 沒有定义任何变量。使用未定义变量会引发错误。
[] // x以传值方式传入(默认),y以引用方式传入。
[x, &y] // 全部引用捕获
[&] // 全部值捕获
[=] // x显式值捕获
[&, x] // z显式引用捕获
[=, &z] this] // 捕获对象的this指针 [
注意点:
捕获引用时必须要注意外部变量的生命周期,防止变量失效
int main()
{100,y=200,z=300;
int x = // x是值传递,y和z是引用传递
auto ff = [=,&y,&z](int n) {
cout <<x << endl;
y++; z++;
return n*n;
};15) << endl;
cout << ff("," << z << endl;
cout << y << }
this 指针是一种特殊情况,它不能被直接值捕获。this 指针只能隐式引用捕获
捕获this指针也可以使用 [this], [=]或者 [&]
。在上述任何情况下,类内数据成员(包括 private)的访问方式与常规方法一样。
class Example
{public:
m_var(10) {}
Example() :
void func()
{
[=]()std::cout << m_var << std::endl; }(); //立即调用
{
}
private:
m_var;
int
};
int main()
{
Example e;
e.func(); }
和预处理阶段一样,在这里你也可以“面向编译器编程”,用一些指令或者关键字让编译器按照你的想法去做一些事情。
虽然编译器非常聪明,但因为 C++ 语言实在是太复杂了,偶尔它也会“自作聪明”或者“冒傻气”。如果有这么一个东西,让程序员来手动指示编译器这里该如何做、那里该如何做,就有可能会生成更高效的代码。
在 C++11 之前,标准里没有规定这样的东西,但 GCC、VC 等编译器发现这样做确实很有用,于是就实现出了自己“编译指令”,在 GCC 里是__ attribute __
,在 VC 里是__declspec
。不过因为它们不是标准,所以名字显得有点“怪异”。
到了 C++11,标准委员会终于认识到了编译指令的好处,于是就把“民间”用法升级为“官方版本”,起了个正式的名字叫“属性”。你可以把它理解为给变量、函数、类等“贴”上一个编译阶段的“标签”,方便编译器识别处理。
C++11:noreturn 、carries_dependency
C++14:deprecated
目前的 C++17 和 C++20 又增加了五六个新属性,比如 fallthrough、likely,但我觉得,标准委员会的态度还是太“保守”了,在实际的开发中,这些真的是不够用。
// 编译的时候用到这个函数的地方会报警告
deprecated("deadline:2020-12-31")]] // C++14 or later
[[ int old_func();
好在“属性”也支持非标准扩展,允许以类似名字空间的方式使用编译器自己的一些“非官方”属性,比如,GCC 的属性都在“gnu::”里。下面我就列出几个比较有用的:
* deprecated:与 C++14 相同,但可以用在 C++11 里。
* unused:用于变量、类型、函数等,表示虽然暂时不用,但最好保留着,因为将来可能会用。
* constructor:函数会在 main() 函数之前执行,效果有点像是全局对象的构造函数。
* destructor:函数会在 main() 函数结束之后执行,有点像是全局对象的析构函数。
* always_inline:要求编译器强制内联函数,作用比 inline 关键字更强。
* hot:标记“热点”函数,要求编译器更积极地优化。
在没有这个属性的时候,如果有暂时用不到的变量,我们只能用(void) var;
的方式假装用一下,来“骗”过编译器,属于“不得已而为之”的做法。
那么现在,我们就可以用“unused”属性来清楚地告诉编译器:这个变量我暂时不用,请不要过度紧张,不要发出警告来烦我:
gnu::unused]] // 声明下面的变量暂不使用,不是错误
[[ int nouse;
属性”像是给编译器的一个“提示”“告知”,无法进行计算,还算不上是编程,而“静态断言”,就有点编译阶段写程序的味道了。
assert 虽然是一个宏,但在预处理阶段不生效,而是在运行阶段才起作用,所以又叫“动态断言”。
static_assert 可以在编译阶段定义各种前置条件,充分利用 C++ 静态类型语言的优势,让编译器执行各种检查,避免把隐患带到运行阶段。在编译阶段计算常数和类型,如果断言失败就会导致编译错误。它也是迈向模板元编程的第一步。
static_assert 运行在编译阶段,只能看到编译时的常数和类型,看不到运行时的变量、指针、内存数据等,所以不要简单地把 assert 的习惯搬过来用。
注意点:在用“静态断言”的时候,你就要在脑子里时刻“绷紧一根弦”,把自己代入编译器的角色,像编译器那样去思考,看看断言的表达式是不是能够在编译阶段算出结果。
比如:
比如说,这节课刚开始时的斐波拉契数列计算函数,可以用静态断言来保证模板参数必须大于等于零:
template<int N>
struct fib
{0, "N >= 0");
static_assert(N >=
static const int value =1>::value + fib<N - 2>::value;
fib<N - };
不过这句话说起来容易做起来难,计算数字还好说,在泛型编程的时候,怎么检查模板类型呢?比如说,断言是整数而不是浮点数、断言是指针而不是引用、断言类型可拷贝可移动……
这些检查条件表面上看好像是“不言自明”的,但要把它们用 C++ 语言给精确地表述出来,可就没那么简单了。所以,想要更好地发挥静态断言的威力,还要配合标准库里的type_traits
,它提供了对应这些概念的各种编译期“函数”。
// 假设T是一个模板参数,即template<typename T>
static_assert(
"int");
is_integral<T>::value, static_assert(
"ptr");
is_pointer<T>::value, static_assert(
"constructible"); is_default_constructible<T>::value,
实现编译期的整数序列,如下例make_index_sequence<3>()会使fun函数的模板参数: int... N 推演为:0,1,2序列 :
#include <iostream>
#include <tuple>
using namespace std;
template<int... N>
decltype(auto) fun(index_sequence<N...> is) {
return make_tuple(N...);
}
int main() {
3>());
auto t = fun(make_index_sequence<std::get<0>(t) << endl;
cout << std::get<1>(t) << endl;
cout << std::get<2>(t) << endl;
cout << 0;
return }
用于泛型编程中的条件判断
template <int N, int... Ns>
auto sum()
{0 == sizeof...(Ns))
if constexpr (
return N;
else
return N + sum<Ns...>();
}
// 调用
1, 2, 3>(); // returns 6 sum<
要确保用 new 动态分配的内存空间在程序的各条执行路径都能被释放是一件麻烦的事情。C++ 11 模板库的 <memory>
头文件中定义的智能指针,即 shared_ptr 模板,就是用来部分解决这个问题的。
托管 p 的 shared_ptr 对象在消亡时会自动执行delete p。而且,该 shared_ptr 对象能像指针 p —样使用,即假设托管 p 的 shared_ptr 对象叫作 ptr,那么 *ptr
就是 p 指向的对象。
只有指向动态分配的对象的指针才能交给 shared_ptr 对象托管。将指向普通局部变量、全局变量的指针交给 shared_ptr 托管,编译时不会有问题,但程序运行时会出错,具体如下:
1. 局部变量: 局部变量在函数内部声明,其生命周期仅限于函数的执行。一旦函数执行结束,局部变量将被销毁,但 shared_ptr
通常会在更长的时间范围内保持活动,因为它们在引用计数为零时才会释放所管理的内存。如果将 shared_ptr
指向局部变量,可能会导致在变量超出作用域后继续访问已释放的内存。
2. 全局变量: 全局变量在整个程序的生命周期内存在。由于它们的生命周期超过了引用计数的范围,shared_ptr
不适合用于管理全局变量的内存。全局变量的生命周期通常在程序的开始和结束之间,而 shared_ptr
在对象不再需要时释放内存,这两种生命周期的匹配不适合。
注意
1. 不能用下面的方式使得两个 shared_ptr 对象托管同一个指针,这样可能会出现double free
10);
A* p = new A( shared_ptr <A> sp1(p), sp2(p);
2. 不要轻易的使用get()
获得raw pointer,然后操作它
C++ 里也是有垃圾回收的,不过不是 Java、Go 那种严格意义上的垃圾回收,而是广义上的垃圾回收,这就是构造 / 析构函数和 RAII 惯用法(Resource Acquisition Is Initialization)。
我们可以应用代理模式,把裸指针包装起来,在构造函数里初始化,在析构函数里释放。这样当对象失效销毁时,C++ 就会自动调用析构函数,完成内存释放、资源回收等清理工作。
智能指针就是代替你来干这些“脏活累活”的。它完全实践了 RAII,包装了裸指针,而且因为重载了 *
和 ->
操作符,用起来和原始指针一模一样
"hello")); // string智能指针
unique_ptr<string> ptr2(new string(assert(*ptr2 == "hello"); // 可以使用*取内容
assert(ptr2->size() == 5); // 可以使用->调用成员函数
unique_ptr 虽然名字叫指针,用起来也很像,但它实际上并不是指针,而是一个对象。所以,不要企图对它调用 delete,它会自动管理初始化时的指针,在离开作用域时析构释放内存。
另外,它也没有定义加减运算,不能随意移动指针地址,这就完全避免了指针越界等危险操作
// 导致编译错误
ptr1++; 2; // 导致编译错误 ptr2 +=
易犯错误:不初始化,而是声明后直接使用
int> ptr3; // 未初始化智能指针
unique_ptr<42 ; // 错误!操作了空指针 *ptr3 =
为了避免这种低级错误,你可以调用工厂函数 make_unique()
,强制创建智能指针的时候必须初始化。同时还可以利用自动类型推导的 auto,少写一些代码:
int>(42); // 工厂函数创建智能指针
auto ptr3 = make_unique<assert(ptr3 && *ptr3 == 42);
"god of war"); // 工厂函数创建智能指针
auto ptr4 = make_unique<string>(assert(!ptr4->empty());
make_unique() 要求 C++14,你可以自己实现一个:
template<class T, class... Args> // 可变参数模板
std::unique_ptr<T> // 返回智能指针
// 可变参数模板的入口参数
my_make_unique(Args&&... args)
{std::unique_ptr<T>( // 构造智能指针
return std::forward<Args>(args)...)); // 完美转发
new T( }
unique_ptr不允许共享,任何时候只能有一个“人”持有它。故禁止了拷贝和赋值,所以,在向另一个 unique_ptr 赋值的时候,要特别留意,必须用std::move()
函数显式地声明所有权转移。
不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr。上面的my_make_unique就是return了一个unique_ptr
shared_ptr它的所有权是可以被安全共享的,也就是说支持拷贝赋值,允许被多个“人”同时持有,就像原始指针一样
int>(42); // 工厂函数创建智能指针
auto ptr1 = make_shared<assert(ptr1 && ptr1.unique() ); // 此时智能指针有效且唯一
// 直接拷贝赋值,不需要使用move()
auto ptr2 = ptr1; assert(ptr1 && ptr2); // 此时两个智能指针均有效
assert(ptr1 == ptr2); // shared_ptr可以直接比较
// 两个智能指针均不唯一,且引用计数为2
assert(!ptr1.unique() && ptr1.use_count() == 2);
assert(!ptr2.unique() && ptr2.use_count() == 2);
shared_ptr 支持安全共享的秘密在于内部使用了“引用计数”,当引用计数减少到0才真正的释放。
因为 shared_ptr 具有完整的值语义(即可以拷贝赋值),所以,它可以在任何场合替代原始指针,而不用再担心资源回收的问题,比如用于容器存储指针、用于函数安全返回动态创建的对象,等等
注意点:
- shared_ptr 有少量的管理成本,也会引发一些难以排查的错误,所以不要过度使用。
- 因为我们把指针交给了 shared_ptr 去自动管理,但在运行阶段,引用计数的变动是很复杂的,很难知道它真正释放资源的时机。你要特别小心对象的析构函数,不要有非常复杂、严重阻塞的操作。一旦 shared_ptr 在某个不确定时间点析构释放资源,就会阻塞整个进程或者线程,“整个世界都会静止不动”
- shared_ptr 的引用计数也导致了一个新的问题,就是“循环引用”,这在把 shared_ptr作为类成员的时候最容易出现,典型的例子就是链表节点。
它专门为打破循环引用而设计,只观察指针,不会增加和减少引用计数,但在需要的时候,可以调用成员函数 lock()
,升级到shared_ptr。
C++11标准虽然将 weak_ptr 定位为智能指针的一种,但该类型指针通常不单独使用,因为连*
、 ->
都没重载),只能和 shared_ptr 类型指针搭配使用。甚至于,我们可以将 weak_ptr 类型指针视为 shared_ptr 指针的一种辅助工具,借助 weak_ptr 类型指针, 我们可以获取 shared_ptr 指针的一些状态信息,比如有多少指向相同的 shared_ptr 指针、shared_ptr 指针指向的堆内存是否已经被释放等等。
weak_ptr<T>
模板类中没有重载 *
和 ->
运算符,这也就意味着,weak_ptr 类型指针只能访问所指的堆内存,而无法修改它。
成员方法:
operator=() //重载 = 赋值运算符,是的 weak_ptr 指针可以直接被 weak_ptr 或者 shared_ptr 类型指针赋值。
// 其中 x 表示一个同类型的 weak_ptr 类型指针,该函数可以互换 2 个同类型 weak_ptr 指针的内容。
swap(x) // 将当前 weak_ptr 指针置为空指针。
reset() //查看指向和当前 weak_ptr 指针相同的 shared_ptr 指针的数量。
use_count() // 判断当前 weak_ptr 指针为否过期(指针为空,或者指向的堆内存已经被释放)。
expired() // 如果当前 weak_ptr 已经过期,则该函数会返回一个空的 shared_ptr 指针;反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针。 lock()
循环引用
在把 shared_ptr作为类成员的时候最容易出现,典型的例子就是链表节点
class Node final
{public:
this_type = Node;
using shared_type = std::shared_ptr<this_type>;
using public:
shared_type next; // 使用智能指针来指向下一个节点
};
// 工厂函数创建智能指针
auto n1 = make_shared<Node>(); // 工厂函数创建智能指针
auto n2 = make_shared<Node>();
assert(n1.use_count() == 1); // 引用计数为1
assert(n2.use_count() == 1);
// 两个节点互指,形成了循环引用
n1->next = n2;
n2->next = n1;assert(n1.use_count() == 2); // 引用计数为2
assert(n2.use_count() == 2);
// 出作用域析构时n1减1,n2减1,但因彼此都相互引用,所以无法减到0
改正方式
class Node final
{public:
this_type = Node;
using // 注意这里,别名改用weak_ptr
shared_type = std::weak_ptr<this_type>;
using public:
shared_type next; // 因为用了别名,所以代码不需要改动
};// 工厂函数创建智能指针
auto n1 = make_shared<Node>(); // 工厂函数创建智能指针
auto n2 = make_shared<Node>(); // 两个节点互指,形成了循环引用
n1->next = n2;
n2->next = n1;assert(n1.use_count() == 1); // 因为使用了weak_ptr,引用计数为1
assert(n2.use_count() == 1); // 打破循环引用,不会导致内存泄漏
// 检查指针是否有效
if (!n1->next.expired()) { // lock()获取shared_ptr
auto ptr = n1->next.lock();
assert(ptr == n2); }
final
final 用于修饰类、函数或虚函数,表示它们不能被继承、重写或覆盖。
修饰类:标记一个类为最终类,禁止其他类继承它。
修饰虚函数:表示该虚函数在派生类中不能被重写。
class Base final { // 声明 Base 类为最终类
virtual void foo() const final; // foo() 不能在派生类中重写
};
delete
delete 用于修饰特殊成员函数(如构造函数、析构函数和赋值运算符等),表示禁用默认生成的该成员函数。
用于禁用某个函数,通常是为了阻止不应该被调用的操作。
class NoCopy {
public:
default;
NoCopy() = const NoCopy&) = delete; // 禁用拷贝构造函数
NoCopy( };
explicit
explicit 用于修饰类的单参数构造函数,表示禁止编译器进行隐式类型转换。用于避免不意料的类型转换,提高代码的明确性和可读性。
class MyClass {
public:
explicit MyClass(int value) : memberValue(value) {}
private:
int memberValue;
};
override
override 用于修饰派生类中的虚函数,显式声明该函数是对基类虚函数的重写。
用于帮助编译器检测是否真正覆盖了基类的虚函数。
class Base {
public:
virtual void foo();
};
class Derived : public Base {
public:
void foo() override; // 显式声明 foo() 是对基类虚函数的重写
};
这些关键字在 C++ 中用于表达特定的语义和约束,能够帮助程序员在代码中传达更多的意图并避免潜在的错误。
在enum没出现之前,enum的功能则需要需要一系列的#define来完成,而enum则完成了这一系列#define的“打包收集”,即:
enum Color {black, white, red};
#define black 0
#define white 1
#define red 2
也正是如此,对于两个不一样的枚举体,它们即使枚举体的名字不同,里面的内容也不能重名,重名编译器就懵逼了。
在Effective modern C++中Item 10: Prefer scoped enums to unscoped enum,调到要用有范围的enum class代替无范围的enum。
例如:
enum Shape {circle,retangle};10; // error auto circle =
上述错误是因为两个circle在同一范围。
对于enum等价于
#define circle 0
#define retangle 1
因此后面再去定义circle就会出错。
所以不管枚举名是否一样,里面的成员只要有一致的,就会出问题。例如:
enum A {a,b}; enum B {c,a};
a出现两次,在enum B的a处报错。
根据前面我们知道,enum名在范围方面没有什么作用,因此我们想到了namespace,如下例子:
// 在创建枚举时,将它们放在名称空间中,以便可以使用有意义的名称访问它们:
namespace EntityType {enum Enum {0,
Ground =
Human,
Aerial,
Total
};
}
void foo(EntityType::Enum entityType)
{
if (entityType == EntityType::Ground) {/*code*/
} }
但是不断的使用命名空间,势必太繁琐,因此在c++11后,引入enum class。它解决了为enum成员定义类型、类型安全、约束等问题。回到上述例子:
// enum class
enum class EntityType {0,
Ground =
Human,
Aerial,
Total
};
void foo(EntityType entityType)
{
if (entityType == EntityType::Ground) {/*code*/
} }
枚举通常用某种整数类型表示,这个类型被称为枚举的基础类型。基础类型默认是int,也可以显式的指定:
#include <iostream>
#include <string>
enum class Color:int {red,green,blue};
enum class Font:char {normal,bold};
int main()
{
Color c;std::cout<<sizeof(c)<<std::endl; //输出为 4
Font f;std::cout<<sizeof(f)<<std::endl; //输出为 1
}
类似rust Result<T, E>
场景:如果有这样一个函数,通过返回值来判断计算结果是否有效,如果结果有效,才能使用结果
bool div_int(int a, int b, int &result) {0) {
if (b ==
return false;
}
result = a / b;
return true; }
这样的使用方式很不方便,需要两个变量来描述结果。这种场景下应该使用c++17中的std::optional。
//div_int可以通过optional优化:optional中,结果是否有效和结果都保存在其中
std::optional<int> div_int(int a, int b) {
0) {
if (b != std::make_optional<int>(a / b);
return
}
return {};
}
TEST_F(optional) {2, 1);
auto ret = div_int(
ASSERT(ret);2, ret.value()); // 如果ret为true, 直接从ret中获取结果
ASSERT_EQ(
2, 0);
auto ret2 = div_int(// 结果无效
ASSERT(!ret2);
// 如果ret2为false,获取访问value将会 抛出异常
try {
ret2.value();std::exception e) {
} catch (std::cout << e.what() << std::endl;
} }
引入右值引用的主要目的是提高程序运行的效率。有些对象在复制时需要进行深拷贝,深拷贝往往非常耗时。合理使用右值引用可以避免没有必要的深复制操作。
class A{};//错误,无名临时变量 A() 是右值,因此不能初始化左值引用 r1
A & rl = A(); //正确,因 r2 是右值引用 A && r2 = A();
通常情况下,判断某个表达式是左值还是右值,最常用的有以下 2 种方法:
* 可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值
* 有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。
所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员(动态分配内存)的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。
我们知道,非 const 右值引用只能操作右值,程序执行结果中产生的临时对象(例如函数返回值、lambda 表达式等)既无名称也无法获取其存储地址,所以属于右值。
当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。
在实际开发中,通常在类中自定义移动构造函数的同时,会再为其自定义一个适当的拷贝构造函数,由此当用户利用右值初始化类对象时,会调用移动构造函数;使用左值(非右值)初始化类对象时,会调用拷贝构造函数。
只有移动没有拷贝的例子:
1. std::unique_ptr<>
2. std::mutex
移动构造和移动赋值是c++实现零拷贝的利器。对于一个简单的用户类,在满足一定条件时,编译器会提供默认的移动构造和移动赋值实现:默认实现会对类成员变量递归地进行移动构造和移动赋值。如果用户类内包含STL容器这种本身有移动实现的成员变量,又不包含非RAII的资源管理,那么默认移动逻辑是完全够用的。
但是,启用默认移动构造函数必须满足以下全部条件:
1. 没有声明拷贝赋值函数。
2. 没有声明拷贝构造函数。
3. 没有声明移动赋值函数。
4. 移动构造函数没有隐式声明为delete(参考这里,简单类一般不需要考虑)。
5. 没有声明析构函数。
同时,启用默认移动赋值函数必须满足以下全部条件:
1. 没有声明拷贝赋值函数。
2. 没有声明拷贝构造函数。
3. 没有声明移动构造函数。
4. 移动赋值函数没有隐式声明为delete(参考这里,简单类一般不需要考虑)。
5. 没有声明析构函数。
关于第5点其实也很容易理解。因为析构函数不可省略的场景往往涉及资源的手动分配和释放,这时候移动构造和移动赋值一般需要进行诸如交接所有权的额外逻辑。所以在用户类已声明析构函数时,编译器不提供默认的移动逻辑以降低非预期行为的可能性。
move 本意为 "移动",但该函数并不能移动任何数据,它的功能很简单,就是将某个左值强制转化为右值。
std::vector<int> v1;
1);
v1.push_back(2);
v1.push_back(
// Move the contents of v1 to v2 using move semantics
std::vector<int> v2 = std::move(v1);
C++ 中的容器 emplace 系列函数,如 emplace_back、emplace 等,允许你直接在容器中构造元素,而不需要进行显式的拷贝或移动操作。这些函数能够减少拷贝或移动的开销,因为它们直接在容器内部构造元素,而不需要创建临时对象。
struct Foo {
Foo(int n, double x);
};
std::vector v;
42, 3.1416); // 没有临时变量产生
v.emplace_back(42, 3.1416)); // 需要产生一个临时变量
v.insert(someIterator, Foo(42, 3.1416}); // 需要产生一个临时变量
v.insert(someIterator, {
std::map<int, std::string> myMap;
1, "one"); // 构造一个键值对并插入 myMap.emplace(
做到这一点主要 使用了 C++11 的两个新特性 变参模板变参模板 和 完美转发完美转发。
变参模板使得 emplace 可以接受任意参数,这样就可以适用于任意对象的构建。
完美转发使得接收下来的参数 能够原样的传递给对象的构造函数
完美转发,它指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。
C++11 标准中规定,通常情况下右值引用形式的参数只能接收右值,不能接收左值。但对于函数模板中使用右值引用语法定义的参数来说,它不再遵守这一规定,既可以接收右值,也可以接收左值(此时的右值引用又被称为万能引用)。
在实现完美转发时,只要函数模板的参数类型为 T&&
,则 C++ 可以自行准确地判定出实际传入的实参是左值还是右值。
通过将函数模板的形参类型设置为 T&&
,我们可以很好地解决接收左、右值的问题。但除此之外,还需要解决一个问题,即无论传入的形参是左值还是右值,对于函数模板内部来说,形参既有名称又能寻址,因此它都是左值。那么如何才能将函数模板接收到的形参连同其左、右值属性,一起传递给被调用的函数呢?
总的来说,在定义模板函数时,我们采用右值引用的语法格式定义参数类型,由此该函数既可以接收外界传入的左值,也可以接收右值;
其次,还需要使用 C++11 标准库提供的 forword()
模板函数修饰被调用函数中需要维持左、右值属性的参数。由此即可轻松实现函数模板中参数的完美转发。
template <typename T>
void function(T&& t) {
otherdef(forward<T>(t)); }
所谓常量表达式,指的就是由多个(≥1)常量组成的表达式。常量表达式一旦确定,其值将无法修改。
我们知道,C++ 程序的执行过程大致要经历编译、链接、运行这 3 个阶段。值得一提的是,常量表达式和非常量表达式的计算时机不同,非常量表达式只能在程序运行阶段计算出结果;而常量表达式的计算往往发生在程序的编译阶段,这可以极大提高程序的执行效率,因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间。
对于用 C++ 编写的程序,性能往往是永恒的追求。那么在实际开发中,如何才能判定一个表达式是否为常量表达式,进而获得在编译阶段即可执行的“特权”呢?除了人为判定外,C++11 标准还提供有 constexpr 关键字。
constexpr 关键字的功能是使指定的常量表达式获得在程序编译阶段计算出结果的能力,而不必等到程序运行阶段。C++ 11 标准中,constexpr 可用于修饰普通变量、函数(包括模板函数)以及类的构造函数。
注意,获得在编译阶段计算出结果的能力,并不代表 constexpr 修饰的表达式一定会在程序编译阶段被执行,具体的计算时机还是编译器说了算。
在C++ 98之前const是有两种语义的:1.只读 2.常量
C++ 11标准中,为了解决 const 关键字的双重语义问题,保留了 const 表示“只读”的语义,而将“常量”的语义划分给了新添加的 constexpr 关键字。因此 C++11 标准中,建议将 const 和 constexpr 的功能区分开,即凡是表达“只读”语义的场景都使用 const,表达“常量”语义的场景都使用 constexpr。
只读”和“常量”之间并没有必然的联系
int main()
{10;
int a =
const int & con_b = a;
cout << con_b << endl;20;
a =
cout << con_b << endl; }
程序中用 const 修饰了 con_b 变量,表示该变量“只读”,即无法通过变量自身去修改自己的值。但这并不意味着 con_b 的值不能借助其它变量间接改变,通过改变 a 的值就可以使 con_b 的值发生变化。
总的来说在 C++ 11 标准中,const 用于为修饰的变量添加“只读”属性;而 constexpr 关键字则用于指明其后是一个常量(或者常量表达式),编译器在编译程序时可以顺带将其结果计算出来,而无需等到程序运行阶段,这样的优化极大地提高了程序的执行效率。
无法区分哪个参数用来构造 key 哪些用来构造 value, 比如key可能需要两个参数构造,value只需要一个参数构造
map<string, complex> scp;1, 2); scp.emplace(“hello”,
解决:用tuple组合起来已进行区分
double>> scp;
map<string, complex<
scp.emplace(piecewise_construct,"hello"),
forward_as_tuple(1, 2)); forward_as_tuple(
所以对于 map 来说你虽然避免了临时变量的构造,但是你却需要构建两个 tuple 。 这种 traedoff 是否值得需要代码编写者自己考虑,从方便性和代码优雅性上来说:
"world", {1, 2}}); scp.insert({
这种初始化方式可以在类的构造函数中省略对成员变量的初始化操作,而让编译器自动在构造对象时进行初始化。
成员变量类内初始化是在编译器生成构造函数的过程中工作的。具体来说,编译器会将成员变量的初始化代码合并到构造函数中,以确保对象在创建时被正确初始化。
构造函数生成: 当编译器处理类的定义时,它会自动生成默认构造函数(如果没有显式定义构造函数)。对于有成员变量类内初始化的情况,编译器会将成员变量的初始值合并到默认构造函数中。
class MyClass {
public:
// 成员变量类内初始化
int value1 = 0;
double value2 = 3.14;
std::string text = "Hello";
// 构造函数
MyClass() {std::cout << "Constructor called" << std::endl;
} };
在C++11以前,初始化存在一系列问题,包括:
4种初始化方式:X t1 = v;、X t2(v);、X t3 = { v };、X t4 = X(v);;
C++ 11努力创造一个统一的初始化方式。其语法是使用{}和std::initializer_list
列表初始化分为两类:直接初始化与拷贝初始化。
原理
针对形如"{ 1, 2, 3 }"的参数列表,系统会首先自动调用参数初始化(value initialization),将其转换成一个std::initializer_list
使用std::initializer_list对象来初始化std::vector类的构造函数
一些例子:
int x = 10; // 使用传统初始化方式
int y{20}; // 使用统一初始化语法
struct Point {
int x;
int y;
};
2, 3}; // 聚合类型初始化
Point p = {4, 5}; // 统一初始化语法
Point q{
std::vector<int> nums = {1, 2, 3}; // 使用统一初始化初始化容器
std::vector<int> nums{1, 2, 3}; // 使用统一初始化初始化容器
std::string str{"Hello, World!"}; // 使用统一初始化初始化字符串
在 C++11 里,你就可以使用“委托构造”的新特性,一个构造函数直接调用另一个构造函数,把构造工作“委托”出去,既简单又高效
#include <iostream>
class MyClass {
public:
// 委托构造函数,调用本类的两个参数的构造函数
0, 0) {
MyClass() : MyClass(std::cout << "Default constructor" << std::endl;
}// 委托构造函数,调用本类的两个参数的构造函数
int value) : MyClass(value, 0) {
MyClass(std::cout << "Single-argument constructor" << std::endl;
}
int value1, int value2) : memberValue1(value1), memberValue2(value2) {
MyClass(std::cout << "Two-argument constructor" << std::endl;
}
void PrintValues() {
std::cout << "Values: " << memberValue1 << ", " << memberValue2 << std::endl;
}
private:
int memberValue1;
int memberValue2;
};
书写多个派生类构造函数只为传递参数完成基类初始化,这种方式无疑给开发人员带来麻烦,降低了编码效率。从 C++11 开始,推出了继承构造函数(Inheriting Constructor),使用 using 来声明继承基类的构造函数,我们可以这样书写。
class Base {
public:int value) : baseValue(value) {}
Base(void showValue() {
"Base value: " << baseValue << std::endl;
std::cout <<
}
private:int baseValue;
};
class Derived : public Base {
public:// 继承基类的构造函数
using Base::Base; void showDerivedValue() {
"Derived value: " << baseValue * 2 << std::endl;
std::cout <<
}
};
int main() {
5);
Derived derivedObj(// 调用基类的成员函数
derivedObj.showValue(); // 调用派生类的成员函数
derivedObj.showDerivedValue();
return 0;
}
在写类的时候,我们经常会用到很多外部类型,比如标准库里的 string、vector,还有其他的第三方库和自定义类型。这些名字通常都很长(特别是带上名字空间、模板参数),书写起来很不方便,这个时候我们就可以在类里面用 using 给它们起别名,不仅简化了名字,同时还能增强可读性。
uint_t = unsigned int; // using别名
using uint_t; // 等价的typedef typedef unsigned int
变量的类型名特别长,使用 auto 就会很方便
有时我们希望从变量或表达式推断出要定义的变量类型,decltype 关键字可以用于求表达式的类型
int i;
double t;
struct A { double x; };
const A* a = new A();decltype(a) x1; //x1 是 A*
decltype(i) x2; //x2 是 int
decltype(a->x) x3; // x3 是 double
1.创建包含变量引用的tuple 2.对tuple解构
// std::tie 返回一个元组,其中包含了从输入参数中提取的元素的**引用**
// std::tie 用于引入字典序比较到结构体
struct S {
int n;std::string s;
float d;operator<(const S& rhs) const {
bool // 比较 n 与 rhs.n,
// 而后为 s 与 rhs.s,
// 而后为 d 与 rhs.d
std::tie(n, s, d) < std::tie(rhs.n, rhs.s, rhs.d);
return
}
};
// 通过tie将tuple中的元素解构至多个变量中
std::make_tuple(3.8, 'A', "Lisa Simpson");
auto student =
double gpa1;
char grade1;std::string name1;
std::tie(gpa1, grade1, name1) = student;
std::tie 可用于解包 std::pair ,因为 std::tuple 拥有从 pair 的转换赋值
bool result;std::tie(std::ignore, result) = set.insert(value);
实现原理
template <typename... Types>
std::tuple<Types&...> tie(Types&... args) {
return std::tuple<Types&...>(args...);
}
tie
函数使用了变长模板参数(variadic templates),这使得它可以接受任意数量的输入参数。std::tuple<Types&...>(args...)
构造了一个元组,其中的每个元素都是输入参数的引用。c++17中引入了std::variant。std::variant类似union
std::variant<int, float, std::string>; // 定义支持int、float、string三个类型,并取一个别名
using IntFloatString = //初始化一个variant
TEST_F(InitVariant) {
10;
IntFloatString i = 10, std::get<int>(i) );
ASSERT_EQ(10, std::get<0>(i) );
ASSERT_EQ(std::cout << i.index(); // prints 0, 即第几个位置设置了值
20.0f;
IntFloatString f = 20.0f, std::get<float>(f) );
ASSERT_EQ(
"hello world";
IntFloatString s = "hello world", std::get<std::string>(s));
ASSERT_EQ( }
//before
void isKeyword(const std::string & lit){
work();
}//after
void isKeyword(std::string_view lit){
work(); }
string存在的问题:
1. 使用std::string的接口,字符串字面值、字符数组、字符串指针的传递仍要数据拷贝
2. substr O(n)复杂度
string_view
是c++17标准库提供的一个类,它提供一个字符串的视图,即可以通过这个类以各种方法“观测”字符串,但不允许修改字符串。它内部只保存一个指针和长度,无论是拷贝,还是修改,都非常廉价
构造和求substr都是O(1)的复杂度。
std的string的构造不可避免的会设计内存分配和拷贝。而string_view只是一个字符串的视图,构造函数可以避免拷贝,做到O(1)复杂度。
更改视图的大小
类中提供了两个函数remove_suffix
(从后面缩减大小)和remove_prefix
(从前方缩减大小),可以缩减视图的大小。
"123456789");
string_view sv(1); // 现在sv中为:12345678, sv的大小为8
sv.remove_suffix(2); // 现在sv中为: 345678, sv的大小为6 sv.remove_prefix(
当然他还提供了其他的一些string具有的方法,方便对原始的字符串进行操作。
注意点:
1. 因为string_view并不拷贝内存,所以要特别注意它所指向的字符串的生命周期。string_view指向的字符串,不能再string_view死亡之前被回收。这跟悬挂指针(dangling pointer)或悬挂引用(dangling references)很像
比如:
string_view foo() {std::string s{"hello world"};
return string_view{s}; }
推荐的使用方式:仅仅作为函数参数,因为如果该参数仅仅在函数体内使用而不传递出去,这样使用是安全的。这样使用,函数的参数可以接收字符串字面值、字符数组、字符串指针、std::string,而不用拷贝
void printLength(std::string_view sv) {
std::cout << "Length of string: " << sv.length() << std::endl;
}
int main() {
std::string s1 = "Hello, world!";
char s2[] = "C++ is great!";
const char* s3 = "Programming is fun!";
// 传递不同类型的字符串给 printLength 函数
// 使用 std::string
printLength(s1); // 使用字符数组
printLength(s2); // 使用字符串指针
printLength(s3); "Short"); // 使用字符串字面值
printLength(
return 0;
}
//before
auto*tmp = parseExpression();
if(tmp!=nullptr){
work();
}//after
if (auto* tmp = parseExpression(); tmp != nullptr) {
work(); }
std::tuple<int,string> nextToken(){
return {4,"fallthrough"};
}
//before
int main() {
auto token = nextToken();
std::cout<<std::get<int>(token)<<","<<std::get<std::string>(token);
return 0;
}//after
int main() {
auto[tokenType,lexeme] = nextToken();
std::cout<<tokenType<<","<<lexeme;
return 0;
}