目录
1. 统一的列表初始化
1.1 {} 初始化
不是初始化列表,在C++98中,允许使用花括号{}对数组或者结构体元素进行统一的列表初始化设定。
C++11扩大了用花括号括起来的列表的使用范围,使其可以用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号,可以不添加,但是不建议去掉。
int x{42}; // 初始化整数
double y{3.14}; // 初始化浮点数
int arr[]{1, 2, 3, 4, 5}; // 初始化数组
struct Point {
int x, y;
};
Point p{10, 20}; // 初始化结构体
std::vector<int> vec{1, 2, 3, 4, 5}; // 初始化 vector
std::map<int, std::string> m{{1, "one"}, {2, "two"}}; // 初始化 map
创建对象时也可以使用列表初始化方式调用构造函数初始化
#include <iostream>
#include <string>
class Date {
private:
int year;
int month;
int day;
public:
// 默认构造函数
Date() : year(1970), month(1), day(1)
{
std::cout << "Default constructor called." << std::endl;
}
void print() const
{
std::cout << year << "-" << month << "-" << day << std::endl;
}
};
int main()
{
// 使用默认构造函数
Date date1{}; // 调用默认构造函数
date1.print(); // 输出: 1970-1-1
// 使用带参数的构造函数
Date date2{2023, 10, 5}; // 调用带参数的构造函数
date2.print(); // 输出: 2023-10-5
return 0;
}
1.2 std::initializer_list
-
定义:
std::initializer_list<T>
是一个模板类,其中T
是列表中元素的类型。它通常用于表示一组相同类型的值。 -
特点:
-
它是一个轻量级的容器,类似于数组。
-
它只提供对元素的只读访问(不能修改元素)。
-
它的生命周期由编译器管理,通常用于临时传递一组值。
-
std::initializer_list
最常见的用途是作为构造函数的参数,以支持列表初始化。
class MyClass
{
public:
// 接受 std::initializer_list 的构造函数
MyClass(std::initializer_list<int> list)
{
for (auto it = list.begin(); it != list.end(); ++it)
{
std::cout << *it << " ";
}
std::cout << std::endl;
}
};
int main()
{
MyClass obj{1, 2, 3, 4, 5}; // 调用 initializer_list 构造函数
return 0;
}
C++11 的标准库容器(如 std::vector
、std::map
等)都支持通过 std::initializer_list
进行初始化。
-
std::initializer_list
实际上是一个轻量级的包装器,底层是一个常量数组。 -
它包含两个指针(或迭代器),分别指向数组的起始位置和结束位置。
-
它的生命周期由编译器管理,通常是一个临时对象。
template<typename T>
class SimpleInitializerList
{
private:
const T* begin_;
const T* end_;
public:
SimpleInitializerList(const T* begin, const T* end) : begin_(begin), end_(end) {}
const T* begin() const { return begin_; }
const T* end() const { return end_; }
};
本质还是调用Initializer_List 的构造函数。
让模拟实现的vector也支持{}初始化和赋值
#include <iostream>
namespace zzy
{
template<class T>
class vector
{
public:
typedef T* iterator;
vector(std::initializer_list<T> lt)
{
reserve(lt.size());
for (auto e : lt)
{
push_back(e);
}
}
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
}
2. 声明
C++11提供了多种简化声明的方式,尤其是在使用模板时。
2.1 auto
这个我们平时用的很多了
2.2 decltype
意为decline type,将变量的类型声明为表达式指定的类型。
decltype
与 auto
的区别:
-
auto
根据初始化值推导类型,并且会忽略引用和const
限定符。 -
decltype
精确推导表达式的类型,包括引用和const
限定符。
int x = 10;
const int& y = x;
auto a = y; // a 的类型是 int
decltype(y) b = y; // b 的类型是 const int&
decltype
不仅可以用于推导表达式的类型,还可以将推导出的类型用于定义变量或作为模板实参,可以单纯定义一个变量出现
int main()
{
int i = 10;
auto p = &i;
auto pf = malloc;
//auto x;
decltype(pf) pf2;
}
也就是说decltype
可以直接推导出一个表达式的类型,并用该类型定义新的变量。
int x = 10;
decltype(x) y = 20; // y 的类型是 int
2.3 nullptr
由于C++中NULL被定义为字面量0,这样就会出现一些问题,因为0既能是指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
3. STL中的一些变化
3.1 新容器
C++11新增了4个,array、forward_list、unorder_map、unordered_list。其中array 和 forward_list 很鸡肋,而unorder_map、unordered_list很有价值。
std::forward_list
的特点
-
单向链表:
std::forward_list
是一个单向链表,每个节点只存储指向下一个节点的指针,没有指向前一个节点的指针。 -
不支持随机访问:
由于是单向链表,std::forward_list
不支持随机访问(如operator[]
或at()
),只能通过迭代器顺序访问。 -
不支持
size()
方法std::forward_list
没有size()
方法,因为它需要遍历整个链表才能计算大小,时间复杂度为 O(n)。如果频繁需要获取链表大小,std::forward_list
会显得非常不方便。 -
虽然
std::forward_list
的内存开销更小,但在大多数场景下,这种优势并不明显。相比之下,std::vector
的缓存友好性和连续内存布局在性能上往往更有优势。
3.2 新接口
cbegin cend crbegin crend这些也是鸡肋的,返回const迭代器实际意义不大,因为begin 和 end 也是可以返回const迭代器的,属于锦上添花的操作。
3.3 所有容器支持{}列表初始化的构造函数
3.4 所有容器新增了emplace系列
我们可以在文档中发现他们使用了右值引用和模板的可变参数,这两项后面介绍。
3.5 新容器增加了移动构造和移动赋值
效率获得很大提升。
4. 右值引用和移动语义
4.1 左值引用和右值引用
传统的C++语法中就有引用,而C++11新增了右值引用的语法特性,所以我们之前学的引用就叫做左值引用,无论左值还是右值引用,都是给对象取别名。
什么是左值?什么是左值引用?
左值是一个表示数据的表达式,我们可以获取它的地址,左值可以出现在赋值符号的左边,右值不能出现在赋值符号的左边。左值引用就是给左值的引用,给左值取别名。
int main()
{
//以下p,b,c,*p都是左值
int *p = new int(0);
int b = 1;
const int c = 2;
//以下是对上面的左值的引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pv = *p;
}
“xxx”也是左值,因为可以取地址,返回的是首元素地址。
什么是右值?什么是右值引用?
右值也是一个数据表达式,如:字面常量,表达式返回值,函数返回值等,右值可以出现在赋值符右边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
int main()
{
double x = 1.1, y = 2.2;
//以下为常见的右值
10;
x+y;
fmin(x,y);
//以下为右值引用
int&& r1 = 10;
double&& r2 = x+y;
double&& r3 = fmin(x,y);
//这里会报错,=左操作数必须是左值
10 = 1;
x+y = 1;
fmin(x,y) = 1;
return 0;
}
右值是不能取地址,但是给右值取别名以后,右值会被储存到特定位置,此时可以对引用后的别名取地址,所以右值引用是左值。
4.2 左值引用和右值引用比较
左值引用总结:
1.左值引用智能引用左值,不能引用右值
2.const左值引用可以引用左值,也可以引用右值
int main()
{
int a = 10;
int& r1 = a;
const int& r2 = 10;
}
const
左值引用是只读的,不能修改绑定的值。因此,即使绑定到右值,也不会导致意外的修改。
普通左值引用(非 const
)不能绑定到右值,因为右值是临时的,而普通左值引用允许修改绑定的值。如果允许普通左值引用绑定到右值,可能会导致以下问题:
-
修改临时对象:
临时对象(右值)的生命周期很短,如果允许修改,可能会导致未定义行为。 -
语义不清晰:
右值通常是临时的,修改右值没有意义。
C++11 引入了右值引用(&&
),专门用于绑定到右值,并支持移动语义(Move Semantics)。右值引用可以修改右值,同时避免不必要的拷贝。
右值引用总结:
1.右值引用只能引用右值,不能引用左值
2.但是右值引用可以引用move以后的左值
int main()
{
int&& r1 = 10;
int a = 10;
//无法将左值绑定到右值引用
int&& r2 = a;
int&& r3 = move(a);
}
4.3 右值引用使用场景和意义
先来看看左值引用的使用场景:
做参数和做返回值都可以提高效率。
void func1(string s)
{}
void func2(string& s)
{}
int main()
{
string s = "hello";
func1(s);
func2(s);
}
在 C++ 中,当一个函数参数是按值传递时(即 void func1(string s)),会创建参数对象的一个副本。对于 std::string 类型,这个副本操作会进行深拷贝,而不是浅拷贝。深拷贝意味着会分配新的内存来存储字符串的内容,而不是仅仅复制指针。
当 func1(s) 被调用时,std::string 的拷贝构造函数会被调用,创建一个新的 std::string 对象,该对象拥有自己独立的内存空间来存储字符串 "hello"。
func2 接受字符串的引用,减少了拷贝,提高了效率。传值返回和传引用返回也是同理。
左值引用的短板:
当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。
string to_string(int value)
{
string str;
//...
return str;
}
int main()
{
string s = to_string(1234);
return 0;
}
str出了to_string就不存在了,如果使用引用返回,引用访问的那块空间已经被销毁了。
传值返回就会面对拷贝构造的问题,老编译器通常会进行两次拷贝构造:一次是将函数内部的临时对象拷贝到返回值,另一次是从返回值拷贝到调用者的变量。新编译器直接在目标位置构造对象,从而实现一次拷贝构造。
右值引用和移动语义就是用来解决上述问题的,那么怎么解决呢?
在这个场景中:
string func()
{
string str("xxxxxxx");
return str;
}
int main()
{
string ret;
ret = func();
return 0;
}
func()的返回值是右值,也就是将亡值(生命周期只在这一行,在向下走就会调析构)。注意,右值分为两种,内置类型的右值是纯右值,自定义类型的右值是将亡值。
ret = 左值:会调用赋值重载:
string& operator=(const string& s)
{
cout << "string& operator=(const string& s)-深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
ret = 左值,这个左值会传给s,因为左值不能凭空消失,不能直接交换,所以要用s拷贝一份tmp,用tmp这个临时变量去交换,然后出作用域后tmp自会销毁。
但 ret = 右值将亡值,这个场景下,将亡值本来也马上销毁了,还不如直接交换了,能省去拷贝tmp的过程:
string& operator=(string&& s)
{
cout << "string& operator=(string&& s)-移动拷贝" << endl;
swap(s);
return *this;
}
其实就是ret把 将亡值 的东西拿过来,反正你也马上销毁了,不要白不要,同时还把ret不用的东西丢给了将亡值,一出作用域直接带走了。
上面这两个代码构成函数重载,我们之前说const左值引用可以引用右值,这里和下面专门的右值引用不会冲突,因为传左值,右值时他们会自己找更匹配的那个。
上面这个就叫移动赋值。
同理,移动构造也是将参数右值的资源拿过来,占为己有,就不用深拷贝了,所以叫他移动构造,就是用别人的资源来构造自己。
这是以前的拷贝构造:
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s)-深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
传过来以后要拷贝一份tmp,用tmp去交换,同样也是因为左值不能随便销毁。
自定义类型的右值反正是将亡值,直接抢:
string(string&& s)
:_str(nullptr)
{
cout << "string(string&& s)-移动语义" << endl;
swap(s);
}
这是我们再来看这个场景:
string func()
{
string str("xxxxxxx");
return str;
}
int main()
{
string ret = func();
return 0;
}
如果编译器不优化:str会拷贝一份给返回值,调用一次拷贝构造,然后由于func()的返回值是将亡值,是右值,通过移动构造函数将临时对象赋值给 ret。
但是编译器在优化以后:str按理来说是左值,按理解应该是进行拷贝构造,但是这样的场景太多了,所以编译器就把str识别成了右值--将亡值。这合理吗?因为str确实符合将亡值的特征,出了作用域会销毁,所以这样的特殊处理是合理的。这样处理以后,从调用拷贝构造变成了调用移动构造。
优化之前是拷贝出的临时对象做右值,优化之后是str直接做右值,更好。
这个场景:
string func()
{
string str("xxxxxxx");
return str;
}
int main()
{
string ret;
ret = func();
return 0;
}
就会变成调用一次移动构造,一次移动赋值。提高了效率。
总结一下:
在 C++11 之前,函数返回一个对象时,通常会发生以下步骤:
-
构造临时对象:
函数内部构造一个临时对象(右值)。 -
拷贝临时对象:
将临时对象拷贝到调用者的目标对象中。 -
销毁临时对象:
临时对象在函数返回后被销毁。
这种拷贝操作在某些情况下(尤其是对象包含动态分配的资源时)会带来显著的性能开销。
C++11 引入了右值引用(&&
)和移动语义,允许将临时对象的资源所有权转移给目标对象,从而避免不必要的拷贝。
通过右值引用和移动语义,传值返回的性能问题得到了显著改善:
-
避免拷贝:
移动语义允许将临时对象的资源直接转移给目标对象,避免了深拷贝的开销。 -
提高性能:
对于包含动态资源的对象(如std::vector
、std::string
等),移动操作通常比拷贝操作快得多。 -
支持返回值优化(RVO):
现代编译器通常会进行返回值优化(Return Value Optimization, RVO),直接构造目标对象,避免临时对象的创建和拷贝。右值引用和移动语义为编译器提供了更多的优化机会。
4.4 右值引用引用左值及其一些更深入的使用场景分析
浅拷贝类本身不拥有独立资源,拷贝后新旧对象共享同一资源,因此不需要通过移动构造函数来转移资源。移动构造不用实现,没有什么需要。
右值引用的场景1:自定义类型中深拷贝的类,必须传值返回的场景
场景2:容器的插入接口,如果插入对象是右值,可以利用移动构造转移资源给数据结构中的对象,也可以减少拷贝
int main()
{
list<string> l;
string s1("1111");
l.push_back(s1);
l.push_back("2222");
//move(s1); 这样写没用
l.push_back(move(s1));
return 0;
}
s1 是左值,在尾插s1时,把s1构造到空间上时用的是拷贝构造string对象。
push_back
的参数类型是 std::string
,编译器会隐式地将 "2222"
转换为一个临时的 std::string
对象。这个临时对象是一个右值,因为它是一个临时创建的、没有名字的对象。
s1 move 以后也是右值,他们会走右值引用,调用移动构造。
4.5 完美转发
模板中的&& 万能引用
void func(int& i)
{
cout << "int& i" << endl;
}
void func(int&& i)
{
cout << "int&& i" << endl;
}
template<typename T>
void PerfectForward(T&& t)
{
func(t);
}
int main()
{
int a = 10;
PerfectForward(a);
PerfectForward(move(a));
return 0;
}
模板中的&&不代表右值引用,而是万能引用,既能接收左值也能接收右值。
模板的万能引用只是提供了能够同时接收左右值引用的能力。
上面代码的结果是:
int& i
int& i
明明我们将左值转移成了右值,但是调用的还是左值引用的函数。
当传递左值 a
给 PerfectForward
时:
-
a
是一个左值,类型为int
。 -
由于
PerfectForward
的参数是通用引用(T&&
),编译器会根据传递的参数推导T
。 -
传递左值时,
T
被推导为int&
。 -
因此,
T&&
实际上是int& &&
。 -
根据引用折叠规则(有 & 为 &),
int& &&
折叠为int&
。
传递右值 move(a) 给 PerfectForward:
- move(a) 是一个右值表达式,类型为 int。
- PerfectForward 的模板参数 T 被推导为 int&&(因为传递的是右值)。
- 因此,T&& t 实际上是 int&& &&。
- 所以,t 的实际类型是 int&&,即右值引用。
前面我们分析过,右值引用其实是一个左值,所以这里t的类型其实是左值,调用了左值引用为参数的func。
但是右值引用被识别成左值是正确的,否则在移动构造的场景下,无法完成资源转移。
string(string&& s)
:_str(nullptr)
{
swap(s);
}
void swap(string& s1)
{
//...
}
右值引用s就得是左值,才能传给s1。
这时就需要完美转发在传参过程中保留对象原生类型属性。
void func(int& i)
{
cout << "int& i" << endl;
}
void func(int&& i)
{
cout << "int&& i" << endl;
}
template<typename T>
void PerfectForward(T&& t)
{
func(forward<T>(t));
}
int main()
{
int a = 10;
PerfectForward(a);
PerfectForward(move(a));
return 0;
}
forward<T>(t) 在传参的过程中保持了t的原生类型属性。
不只在模板函数中,模板类中,但凡想保留属性的都要写一遍forward<T>(t)。
5. 新的类功能
5.1 默认成员函数
原来的C++类中,有6个默认成员函数。
1.构造,2.析构,3.拷贝构造,4.赋值重载,5.取地址重载,6.const取地址重载。
重要的是前4个,C++11新增了两个:移动构造,移动赋值重载。
如果 析构函数、拷贝构造、赋值重载 这3个都没有实现,那么编译器会自动生成一个默认移动构造。默认生成的移动构造,针对内置类型会进行浅拷贝;针对自定义类型成员,如果它实现了移动构造就调用,如果没实现移动构造就调用拷贝构造。
如果 析构函数、拷贝构造、赋值重载 这3个都没有实现,那么编译器会自动生成一个默认移动赋值重载。默认生成的移动赋值,针对内置类型会进行浅拷贝;针对自定义类型成员,如果它实现了移动赋值就调用,如果没实现移动构造就调用赋值重载。
5.2 强制生成默认函数的关键字default
假设我们要使用某个默认的函数,但是由于一些原因这个函数没有生成。比如,我们提供了拷贝构造,就不会生成默认移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
Person(const Person& p)
:_name(p._name)
{}
Person(Person&& p) = default;
5.3 禁止生成默认函数的关键字delete
在C++98中是该函数设置成private,并且只声明,这样类外想调用就会报错,C++11更简单,只需要在该函数声明后加上 =delete 即可。
5.4 继承和多态中的final与override关键字
这个继承和多态章节讲过了。
6. 可变模板参数
C++11的新特性可变参数模板能够让您创建可以接收可变参数的函数模板与类模板,相比C++98,类模板和函数模板中只能含固定数量的模板参数,可变模板参数无疑是一个巨大的改进。
下面就是一个基本可变参数的函数模板:
template<class ...Args>
void func(Args... args)
{
}
Args 是一个模板参数包,是类型,args是一个函数形参参数包,是对象。声明Args... args,这个参数包可以包含0到任意个模板参数。
由于语法不支持使用args[i] 这样的方式获取参数,使用可变模板参数的一个特点也是难点就是如何展开可变模板参数。
递归函数方式展开参数包:
template<class T>
void showList(const T& t)
{
cout << t << endl;
}
template<class T, class ...Args>
void showList(const T& value, const Args&... args)
{
cout << value << endl;
showList(args...);
}
int main()
{
showList(1);
showList(1,'a');
showList(1,'a',"hello");
}
1传给value,a、hello传给args,下一层里a传给value,hello传给args,此时args只有1个参数,则会调用上面的单参数showList,传给t。
逗号表达式展开参数包:
template<class T>
void PrintArg(T t)
{
cout << t << " ";
}
template<class ...Args>
void showList(Args... args)
{
int arr[] = {(PrintArg(args),0)...};
cout << endl;
}
int main()
{
showList(1);
showList(1,'a');
showList(1,'a',"hello");
}
不管几个参数都先进showList,在arr中,在逗号表达式中先走PrintArg(args)将参数输入缓冲区,后走0,最后会创建一个元素值都为0的数组。在构造数组的过程中就将参数包展开了,最后将缓冲区的内容输出出来。这个数组的目的纯粹是为了使用逗号表达式。
STL中的emplace相关接口函数,支持模板的可变参数,并且引入了万能引用。那么相对于insert,emplace系列的优势在哪里呢?
int main()
{
vector<string> vec;
// 使用 insert
vec.insert(vec.end(), string("Hello"));
// 使用 emplace
vec.emplace_back("Hello");
return 0;
}
-
insert
:先构造std::string
临时对象,再将其移动到容器。 -
emplace_back
:直接在容器中构造std::string
,避免了临时对象的创建和移动。
但是在有了移动构造以后,push_back是先构造,再移动构造,emplace_back是直接构造,因为移动构造的效率足够高,所以总体其实也差不多。针对内置类型时,push_back是构造+拷贝构造,emplace_back是直接构造,效率会高一点。
场景 1:构造参数可以直接传递(优先使用 emplace_back
)
假设我们有一个 std::vector<std::string>
,并且我们想直接传递一个字符串字面量(如 "Hello"
)来构造 std::string
对象。这时,emplace_back
是更好的选择,因为它可以直接在容器中构造对象,避免了临时对象的创建和移动。
#include <vector>
#include <string>
int main()
{
std::vector<std::string> vec;
// 使用 emplace_back:直接传递构造参数
vec.emplace_back("Hello"); // 直接在容器中构造 std::string
return 0;
}
场景 2:已经有一个现成的对象(可以使用 push_back
)
假设我们已经有一个 std::string
对象,并且想将它添加到 std::vector
中。这时,push_back
是更自然的选择,因为对象已经存在,不需要再构造。
#include <vector>
#include <string>
int main()
{
std::vector<std::string> vec;
std::string str = "Hello";
// 使用 push_back:添加现成的对象
vec.push_back(str); // 将现有的 std::string 对象添加到容器中
return 0;
}
进一步优化:移动语义
如果对象支持移动语义,且你不再需要原来的对象,可以使用 push_back(std::move(obj))
来避免拷贝:
#include <vector>
#include <string>
int main()
{
std::vector<std::string> vec;
std::string str = "Hello";
// 使用 push_back + std::move:避免拷贝
vec.push_back(std::move(str)); // 移动 str 到容器中,str 变为空
return 0;
}
7. lambada
7.1 lambada 的引入
假设你有一个整数向量,你想根据自定义规则对其进行排序。在没有 Lambda 表达式的情况下,你可能需要定义一个单独的比较函数或函数对象类。使用 Lambda 表达式,你可以直接在排序函数中定义比较逻辑。
#include <iostream>
#include <vector>
#include <algorithm>
// 定义一个比较函数
bool compare(int a, int b)
{
return a > b; // 降序排序
}
int main()
{
std::vector<int> numbers = {4, 2, 5, 3, 1};
// 使用比较函数进行排序
std::sort(numbers.begin(), numbers.end(), compare);
for (int num : numbers)
{
std::cout << num << " ";
}
return 0;
}
int main()
{
std::vector<int> numbers = {4, 2, 5, 3, 1};
// 使用 Lambda 表达式进行排序
std::sort(numbers.begin(), numbers.end(), [](int a, int b) {
return a > b; // 降序排序
});
for (int num : numbers)
{
std::cout << num << " ";
}
return 0;
}
有些地方写仿函数程度太重了,而lambda就很简洁。
lambda的表达式其实是一个匿名函数。
7.2 lambda表达式语法
[capture](parameters)mutable-> return_type { body }
-
capture:捕获列表,用于捕获外部变量以供lambda函数使用
-
parameters:参数列表,与普通函数的参数列表类似。
-
mutable:默认情况下,lambada函数是一个const函数,加上mutable可以取消其常量性,使用该修饰符时,参数列表()不可省略。
-
return_type:返回类型,可以省略,编译器会自动推导。
-
body:函数体,包含具体的逻辑,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意:参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空,因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
int main()
{
int x = 10;
auto add_x = [x](int a)mutable{x *= 2; return a+x;};
cout << add_x(1) << endl;
}
捕捉列表捕捉了x,函数体内可直接使用而不用传参,如果没有mutable,x是无法修改的,但是要实现这个效果用引用捕捉也是可以的。
int x = 10;
auto add_x = [&x](int a){x *= 2; return a+x;};
cout << add_x(1) << endl;
lambda表达式实际上可以理解成无名函数,该函数无法直接调用,如果想调用,可借助auto将其赋值给一个变量。
捕捉列表说明:
- [var]:表示值传递捕捉
- [=]:值传递方式捕捉所有父作用域变量
- [&var]:引用传递捕捉变量var
- [&]:引用方式捕捉所有父作用域变量
- [ 函数]:捕捉函数也是可以的,但是实际中一般不会这样
lambda表达式之间不能互相赋值,即使他们看起来类型相同。
7.3 lambda的底层原理
在C++中,lambda表达式本质上是一个匿名函数对象(即一个闭包类型)。编译器会为每个lambda表达式生成一个唯一的匿名类类型,这个类包含以下部分:
-
成员变量:用于存储捕捉列表中的变量(按值或按引用)。
-
重载的
operator()
:用于实现lambda函数体中的逻辑。
例如,以下lambda表达式:
auto lambda = [x](int y) { return x + y; };
编译器会生成一个类似如下的匿名类:
class __AnonymousLambdaType {
public:
__AnonymousLambdaType(int x) : captured_x(x) {} // 构造函数,初始化捕捉的变量
// 重载的 operator()
int operator()(int y) const
{
return captured_x + y;
}
private:
int captured_x; // 按值捕捉的变量
};
然后,lambda
实际上是一个该类型的对象。
即使两个lambda表达式的签名和捕捉列表完全相同,它们的类型也是不同的,为了不冲突都有自己的nuid作为类名,各不相同。例如:
auto lambda1 = [](int x) { return x + 1; };
auto lambda2 = [](int x) { return x + 1; };
lambda1
和lambda2
的类型是不同的,即使它们的行为完全一致。
在C++中,赋值操作要求左右两边的类型必须相同(或可以隐式转换)。由于每个lambda表达式的类型是唯一的,因此不能直接将一个lambda赋值给另一个lambda。
8. 包装器
8.1 function包装器
std::function
是 C++11 引入的一个通用的函数包装器,它可以存储、复制和调用任何可调用对象(callable object),包括普通函数、lambda 表达式、函数对象(仿函数)、以及类的成员函数等。std::function
提供了一种类型安全的方式来处理各种可调用对象,使得我们可以将它们统一存储和传递。
为什么需要function呢?
在 C++ 中,可调用对象(callable object)的类型多种多样,包括:
-
普通函数
-
函数指针
-
Lambda 表达式等等
这些可调用对象的类型各不相同,直接存储或传递它们会导致代码复杂化。std::function
提供了一种统一的方式来存储和操作这些不同类型的可调用对象。
示例:统一存储不同类型的可调用对象
int add(int a, int b) { return a + b; }
struct Multiply
{
int operator()(int a, int b) const { return a * b; }
};
int main()
{
std::function<int(int, int)> func;
func = add; // 存储普通函数
std::cout << func(2, 3) << std::endl; // 输出 5
func = Multiply(); // 存储函数对象
std::cout << func(2, 3) << std::endl; // 输出 6
func = [](int a, int b) { return a - b; }; // 存储 lambda 表达式
std::cout << func(2, 3) << std::endl; // 输出 -1
return 0;
}
有了包装器是如何解决模板效率低下,实例化多份的问题?
模板的效率问题主要源于代码膨胀(即模板实例化会生成多份代码),而 function
通过类型擦除(type erasure)技术提供了一种统一的接口,从而避免了模板的代码膨胀问题。
模板在编译时会为每种不同的类型生成一份独立的代码。例如:
template <typename F>
void callFunction(F func)
{
func(10);
}
int main()
{
callFunction([](int x) { std::cout << x << std::endl; });
callFunction([](int x) { std::cout << x * 2 << std::endl; });
return 0;
}
在这个例子中,编译器会为每种 lambda 表达式生成一份独立的 callFunction
实例化代码。如果有大量不同类型的可调用对象,会导致代码膨胀,增加编译时间和二进制文件大小。
function
通过类型擦除技术,将不同类型的可调用对象统一存储在一个通用的接口中。这种设计使得 std::function
可以存储任意类型的可调用对象,而不需要为每种类型生成独立的代码。
示例:使用 std::function
避免代码膨胀
void callFunction(std::function<void(int)> func)
{
func(10);
}
int main()
{
callFunction([](int x) { std::cout << x << std::endl; });
callFunction([](int x) { std::cout << x * 2 << std::endl; });
return 0;
}
在这个例子中,无论传递多少种不同的 lambda 表达式,callFunction
只会生成一份代码,因为 function
统一了可调用对象的类型。
包装器的一些其他场景:以逆波兰表达式求值为例
class Solution
{
public:
int evalRPN(vector<string>& tokens)
{
std::stack<int> st;
for(auto& str : tokens)
{
if(!(str=="+" || str=="-" || str=="*" || str=="/"))
{
st.push(stoi(str));
}
else
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
switch(str[0])
{
case '+':
st.push(left + right);
break;
case '-':
st.push(left - right);
break;
case '*':
st.push(left * right);
break;
case '/':
st.push(left / right);
break;
}
}
}
return st.top();
}
};
如果再加一些运算就很麻烦了。
使用包装器以后的玩法:
class Solution
{
public:
int evalRPN(vector<string>& tokens)
{
stack<int> st;
map<string, function<int(int, int)>> funcmap =
{
{"+", [](int i, int j){return i+j;}},
{"-", [](int i, int j){return i-j;}},
{"*", [](int i, int j){return i*j;}},
{"/", [](int i, int j){return i/j;}}
};
for(auto& str: tokens)
{
if(funcmap.find(str) != funcmap.end())
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
st.push(funcmap[str](left, right));
}
else
{
st.push(stoi(str));
}
}
return st.top();
}
};
这种玩法在实践中很多。
8.2 bind
在 C++ 中,std::bind
是一个函数模板,用于将函数或可调用对象与其参数绑定,生成一个新的可调用对象。它通常与 std::function
一起使用,用于实现函数的部分应用(partial application)或延迟调用。
#include <functional>
auto new_callable = std::bind(callable, arg1, arg2, ..., argN);
-
callable
:需要绑定的函数、函数指针、成员函数或可调用对象。 -
arg1, arg2, ..., argN
:绑定到callable
的参数。可以是具体的值、占位符(std::placeholders::_1
,std::placeholders::_2
, ...)或其他可调用对象。 -
返回值:返回一个新的可调用对象
主要用途
-
绑定函数参数:
将函数的某些参数固定,生成一个新的可调用对象。
void print_sum(int a, int b)
{
std::cout << a + b << std::endl;
}
int main()
{
auto bound_func = std::bind(print_sum, 10, std::placeholders::_1);
bound_func(20); // 输出 30(10 + 20)
return 0;
}
2. 重新排列参数顺序:
使用占位符可以改变参数的顺序
void print_values(int a, int b, int c)
{
std::cout << a << ", " << b << ", " << c << std::endl;
}
int main()
{
auto bound_func = std::bind(print_values, std::placeholders::_3, std::placeholders::_1, std::placeholders::_2);
bound_func(1, 2, 3); // 输出 "3, 1, 2"
return 0;
}