跳到主要内容C++11 新特性详解:Lambda、移动语义与可变参数模板 | 极客日志C++算法
C++11 新特性详解:Lambda、移动语义与可变参数模板
C++11 核心特性涵盖 Lambda 表达式简化匿名函数、移动语义提升资源效率、可变参数模板实现泛型编程以及包装器统一可调用对象类型。内容通过实际代码示例解析语法细节与底层原理,帮助开发者掌握现代 C++ 编程范式。
Lambda 表达式
背景与动机
lambda 表达式是一种匿名函数对象,允许在代码中直接定义和使用小型的函数,无需额外定义函数或函数对象类。这种写法虽然理论清晰,但通过特定场景介绍会更直观。
struct Goods {
string _name;
double _price;
int _evaluate;
Goods(const char* str, double price, int evaluate)
:_name(str), _price(price), _evaluate(evaluate) {}
};
struct ComparePriceLess {
bool operator()(const Goods& gl, const Goods& gr) {
return gl._price < gr._price;
}
};
int main() {
vector<Goods> v = {{"苹果", 2.1, 5}, {"香蕉", 3, 4}, {"橙子", 2.2, 3}, {"菠萝", 1.5, 4}};
sort(v.begin(), v.end(), ComparePriceLess());
}
日常生活中,一件商品包含多个特性,若想针对某个特性进行排序,就需要使用算法库里的 sort,设置自定义类型的比较方式,那么仿函数就是个很好的方式。但随着 C++ 语法的发展,人们开始觉得上面的写法太复杂了。每次为了实现一个 algorithm 算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在 C++11 语法中出现了 lambda 表达式。
基本语法结构
lambda 表达式书写格式:
[capture-list] (parameters) mutable -> return-type { statement }
[capture-list]:捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据 [] 来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用。
(parameters):参数列表,与普通函数的参数列表一致,如果不需要参数传递,则可以连同 () 一起省略。
mutable:默认情况下,lambda 函数总是一个 const 函数,mutable 可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->returntype:返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}:函数体,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
值得注意的是,在 lambda 函数定义中,参数列表和返回值类型都是可选忽略部分,而捕捉列表和函数体可以为空。因此 C++11 中最简单的 lambda 函数为:[]{};该 lambda 函数不能做任何事情。
实战应用
int main() {
vector<Goods> v = {{"苹果", 2.1, 5}, {"香蕉", 3, 4}, {"橙子", 2.2, 3}, {"菠萝", 1.5, 4}};
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) -> bool {
return g1._price < g2._price;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) -> bool {
return g1._price > g2._price;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) -> bool {
return g1._evaluate < g2._evaluate;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) -> bool {
return g1._evaluate > g2._evaluate;
});
return 0;
}
因此,lambda 表达式可以这样套用在 sort 里,比仿函数确实方便且可读性更高了。可以看出 lambda 表达式实际是一个匿名函数(无名函数),该函数无法直接调用,如果想要直接调用,可借助 auto 将其赋值给一个变量。
auto ret = [](const Goods& g1, const Goods& g2) { return g1._evaluate < g2._evaluate; };
对于捕捉列表 [],平常一般使用的不多,但是某些情况还是要使用的,需要了解其用法。
捕捉列表描述了上下文中那些数据可以被 lambda 使用,以及使用的方式传值还是传引用:
[x]:表示值传递方式捕捉变量 x。
[=]:表示值传递方式捕获所有父作用域中的变量(包括 this)。
[&x]:表示引用传递捕捉变量 x。
[&]:表示引用传递捕捉所有父作用域中的变量(包括 this)。
[this]:表示值传递方式捕捉当前的 this 指针。
- 父作用域指包含 lambda 函数的语句块。
- lambda 默认以值传递的方式进行,传值捕捉的变量是不可修改的。
int main() {
int x = 10;
auto func = [x]() mutable {
x = 20;
cout << x << std::endl;
};
func();
cout << x << endl;
return 0;
}
使用 mutable 关键字就可以修改了,但是这种修改只是对 lambda 内部的副本进行修改,不会影响到原始的变量。在 main 函数中再次输出 x 时,其值仍为 10。
- 语法上捕捉列表可由多个捕捉项组成,并以逗号分割,比如:
[=, &a, &b],以引用传递的方式捕捉变量 a 和 b,值传递方式捕捉其他所有变量;[&, a, this],值传递方式捕捉变量 a 和 this,引用方式捕捉其他变量。
- 捕捉列表不允许变量重复传递,否则就会导致编译错误。
- 在块作用域以外的 lambda 函数捕捉列表必须为空,在全局作用域中,并没有局部变量可供 lambda 函数捕获。
- 在块作用域中的 lambda 函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
- lambda 表达式之间不能相互赋值,即使看起来类型相同,但是可以拷贝构造(每个 lambda 表达式都有其独特的、未命名的类型。即使两个 lambda 表达式的参数列表和返回类型相同,它们的类型也是不同的)。
底层原理
转到反汇编可以发现,其实 lambda 的本质就是被包装的仿函数,编译器会自动生成一个类,在该类中重载了 operator()。
类的新增特性
移动语义
C++11 新增了两个:移动构造函数和移动赋值运算符重载。
- 如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)。
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
显式控制
class Person {
public:
Person(const char* name = "", int age = 0) : _name(name), _age(age) {}
Person(const Person& p) : _name(p._name), _age(p._age) {}
Person(Person && p) = default;
private:
std::string _name;
int _age;
};
int main() {
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
default 是强制生成默认函数的关键字,我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用 default 关键字显示指定移动构造生成。
class Person {
public:
Person(const char* name = "", int age = 0) : _name(name), _age(age) {}
Person(const Person& p) = delete;
private:
std::string _name;
int _age;
};
int main() {
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
delete 是禁止生成默认函数的关键字,当类显式删除了拷贝构造函数时,编译器不会自动生成移动构造函数(即使没有显式删除移动构造函数),代码中没有显式定义移动构造函数,且隐式移动构造函数被禁用,因此无法完成移动初始化。
移动构造函数的核心目的是高效转移资源所有权(如动态内存、文件句柄等),而拷贝构造函数的目的是创建资源的独立副本。如果一个类禁用了拷贝构造函数,通常意味着:
- 资源不可复制: 例如独占式资源,拷贝会导致资源管理混乱。
- 防止意外拷贝: 开发者希望禁止对象的复制操作,强制使用移动语义。
此时,如果编译器仍然自动生成移动构造函数,可能会破坏这种设计意图。
可变参数模板
概念
其实可变模板参数早在 C 语言就已经有了,后面三个点点点就是可变模板参数,比如:printf("%d,%d,%d", x, y, z),后面的参数个数是可以自己控制有多少个的,这就是一种可变模板参数。
template<class... Args>
void ShowList(Args... args) {}
int main() {
ShowList();
ShowList(1);
ShowList(1, 2.2);
ShowList(1, 2, "xxxxx");
return 0;
}
回到实际定义,Args 是一个模板参数包,args 是一个函数形参参数包,声明一个参数包 Args... args,这个参数包中可以包含 0 到任意个模板参数。
获取个数
template<class... Args>
void ShowList(Args... args) {
cout << sizeof...(args) << endl;
}
int main() {
ShowList();
ShowList(1);
ShowList(1, 2.2);
ShowList(1, 2, "xxxxx");
return 0;
}
展开参数包
不知道当初设计怎么想的,这里想要 for 循环遍历展开是不可行的,编译器不支持,所以这里的展开方法做了解即可。
递归函数
template<class T>
void ShowList(const T& t) {
cout << t << endl;
}
template<class T, class... Args>
void ShowList(T value, Args... args) {
cout << value << " ";
ShowList(args...);
}
int main() {
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', string("sort"));
return 0;
}
模式匹配: 展开函数 ShowList(T value, Args... args) 匹配至少一个参数的情况,每次取出第一个参数 value,剩余参数构成新的参数包 args...,终止函数 ShowList(const T& t) 匹配仅有一个参数的情况,结束递归。
参数包展开: args... 在递归调用时会被解包,每次减少一个参数,直到参数包为空,关键语句 ShowList(args...) 会触发模板的递归实例化,直到匹配终止函数。
输出顺序: 先打印当前参数 value,再递归处理剩余参数,确保参数按传入顺序输出。
逗号表达式
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', string("sort"));
return 0;
}
(PrintArg(args), 0)... 会将参数包 args... 展开为多个表达式,打印对应的值,然后返回 0(用于填充数组)。
int arr[] = {(PrintArg(args), 0)...};
int arr[] = {(PrintArg(1), 0), (PrintArg('A'), 0), (PrintArg("sort"), 0)};
PrintArg 的返回值是 void,无法初始化 int 数组,即使 PrintArg 返回参数类型(如 T),参数包可能包含不同类型(如 int, char),仍会导致类型不匹配。每个元素必须是 int 类型,因此需要用 0 作为统一的返回值,保证初始化的数组元素都为相同类型。
emplace 系列的接口
int main() {
list<pair<int, char>> mylist;
mylist.emplace_back(10, 'a');
mylist.emplace_back(20, 'b');
mylist.emplace_back(make_pair(30, 'c'));
mylist.push_back(make_pair(40, 'd'));
mylist.push_back({50, 'e'});
for(auto e : mylist) cout << e.first << ":" << e.second << endl;
return 0;
}
emplace_back 的作用和 push_back 相同,但是 mylist.emplace_back(20, 'b') 这种格式的写法更方便一些。其实我们会发现其实差别也不大,emplace_back 是直接构造了,push_back 是先构造,再移动构造,移动构造的消耗很小,其实没啥影响。
实际应用
class Date {
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year), _month(month), _day(day) { cout << "Date 构造" << endl; }
Date(const Date& d)
:_year(d._year), _month(d._month), _day(d._day) { cout << "Date 拷贝构造" << endl; }
private:
int _year;
int _month;
int _day;
};
template<class... Args>
Date* Create(Args... args) {
Date* ret = new Date(args...);
return ret;
}
int main() {
list<Date> lt;
Date d(2023, 9, 27);
lt.push_back(d);
lt.emplace_back(d);
lt.emplace_back(2023, 9, 27);
return 0;
}
push_back 只能传日期类对象,emplace_back 既能传日期类对象,又能传日期类对象的参数包。参数包,一路往下传,直接去构造或者拷贝构造节点中日期类对象。
函数包装器
function
template<class F, class T>
T useF(F f, T x) {
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
double f(double i) {
return i / 2;
}
struct Functor {
double operator()(double d) {
return d / 3;
}
};
int main() {
cout << useF(f, 11.11) << endl;
cout << useF(Functor(), 11.11) << endl;
cout << useF([](double d)->double{return d / 4;}, 11.11) << endl;
return 0;
}
我们知道函数指针,仿函数,lambda 表达式,这三种都是函数对象的创建方式,同时调用这三个方式实例化模板,useF 函数模板实例化了三份,明明都是相同的内容,实在是没有必要,会导致模板的效率低下。那么这种时候就需要使用头文件 <functional> 中的 function,function 包装器也叫作适配器。C++ 中的 function 本质是一个类模板,也是一个包装器。
template<class T> function;
template<class Ret, class... Args>
class function<Ret(Args...)>;
- Ret:被调用函数的返回类型。
- Args...:被调用函数的形参。
下面直接修改以上代码,来展示 function 的使用效果:
template<class F, class T>
T useF(F f, T x) {
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
double f(double i) {
return i / 2;
}
struct Functor {
double operator()(double d) {
return d / 3;
}
};
int main() {
function<double(double)> func1 = f;
cout << useF(func1, 11.11) << endl;
function<double(double)> func2 = Functor();
cout << useF(func2, 11.11) << endl;
function<double(double)> func3 = [](double d)->double{return d / 4;};
cout << useF(func3, 11.11) << endl;
return 0;
}
三种可调用对象被统一为同一类型:包装类,模板只实例化一次,静态变量共享(即这个 count 只有一份)。
bind
bind 函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来'适应'原对象的参数列表。
bind 可以理解为一个接收包装类的适配器,上面的例子都是直接将函数给到包装类,那么 bind 就是将特定的函数和参数绑定到包装类,通过例子解析会更容易理解:
int Plus(int a, int b) {
return a + b;
}
class Sub {
public:
int sub(int a, int b) {
return a - b;
}
};
int main() {
std::function<int(int, int)> func1 = std::bind(Plus, placeholders::_1, placeholders::_2);
auto func2 = std::bind(Plus, 1, 2);
cout << func1(1, 2) << endl;
cout << func2() << endl;
Sub s;
std::function<int(int, int)> func3 = std::bind(&Sub::sub, s, placeholders::_1, placeholders::_2);
std::function<int(int, int)> func4 = std::bind(&Sub::sub, s, placeholders::_2, placeholders::_1);
cout << func3(1, 2) << endl;
cout << func4(1, 2) << endl;
return 0;
}
bind 的第一个参数传的是函数,后面的是一系列要传的参数,_1 为第一个参数,_2 为第二个参数,以此类推,参数既可以是待定的,也可以是具体的值,placeholders 属于 std 命名空间,若展开了就不用写。
- 若函数是非静态成员函数,必须在 Sub::sub 前加上 &,因为非静态成员函数依赖对象,必须显式调用其地址,普通函数指针直接指向代码地址,而成员函数指针需要同时包含类的类型信息和函数地址,因此还需要将对象 s 传过去。
- 若函数是静态成员函数,和普通函数一样都是全局函数,就不需要加 & 和传对象。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online