跳到主要内容C++11 核心新特性实战:Lambda、移动语义与模板 | 极客日志C++算法
C++11 核心新特性实战:Lambda、移动语义与模板
综述由AI生成C++11 引入 lambda 表达式简化匿名函数定义,通过移动语义优化资源管理,利用可变参数模板实现泛型编程,并结合 function 和 bind 包装器统一可调用对象接口。文章详细讲解了 Lambda 的语法与捕捉机制、移动构造与 delete/default 关键字的应用、参数包展开技巧以及包装器的实际使用场景。
小熊软糖3 浏览 C++11 核心新特性实战:Lambda、移动语义与模板
1. Lambda 表达式
1.1 引入
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(v.begin(), v.end(), ComparePriceGreater());
}
商品排序需要用到算法库的 sort,设置自定义比较方式时,仿函数确实是个好选择。但随着 C++ 语法演进,这种写法显得过于繁琐。每次为了一个算法逻辑都要重新写个类,如果比较逻辑不同还得实现多个类,命名也容易冲突。于是 C++11 引入了 Lambda 表达式。
1.2 语法
[capture-list] (parameters) mutable -> return-type { statement }
[capture-list]:捕捉列表,总是出现在 Lambda 函数开始位置。编译器根据 [] 判断后续代码是否为 Lambda 函数,它能捕捉上下文中的变量供 Lambda 使用。
(parameters):参数列表,与普通函数一致。若不需要参数,可连同 () 一起省略。
mutable:默认情况下 Lambda 函数是 const 的,mutable 可取消其常量性。使用该修饰符时,参数列表不可省略(即使为空)。
->returntype:返回值类型,用追踪返回类型形式声明。无返回值时可省略。若返回值类型明确,也可由编译器推导省略。
{statement}:函数体,可使用参数及所有捕获到的变量。
注意: 参数列表和返回值类型都是可选忽略的,而捕捉列表和函数体可以为空。因此 C++11 中最简单的 Lambda 函数为 []{},虽然它不能做任何事情。
1.3 使用
有了 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 的可读性和便捷性高了很多。它实际上是一个匿名函数,无法直接调用,但可以用 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 默认以值传递方式工作,传值捕捉的变量在 Lambda 内部是不可修改的。
int main() {
int x = 10;
auto func = [x]() mutable {
x = 20;
cout << x << endl;
};
func();
cout << x << endl;
return 0;
}
加上 mutable 关键字就可以修改了,但这只是修改 Lambda 内部的副本,不会影响原始变量。再次输出 x 时,其值仍为 10。
- 语法上捕捉列表可由多个项组成,逗号分割。例如
[=, &a, &b] 表示引用传递 a 和 b,其他变量值传递;[&, a, this] 表示值传递 a 和 this,引用其他变量。
- 捕捉列表不允许变量重复传递,否则编译报错。
- 块作用域以外的 Lambda 函数捕捉列表必须为空,全局作用域中没有局部变量可供捕获。
- 块作用域中的 Lambda 仅能捕捉父作用域中局部变量,捕捉非此作用域或非局部变量会报错。
- Lambda 表达式之间不能相互赋值,即使类型相同,因为每个 Lambda 都有独特的未命名类型。
1.4 本质
Lambda 的本质是被包装的仿函数。转到反汇编可以看到,编译器会自动生成一个类,在该类中重载了 operator()。
2. 类的新增语法
2.1 移动构造、移动赋值运算符
C++11 新增了移动构造函数和移动赋值运算符重载。如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动构造。对于内置类型成员执行逐字节拷贝,自定义类型成员则看是否实现了移动构造,没实现就调用拷贝构造。
同理,如果你没有实现移动赋值重载,且满足上述条件,编译器也会自动生成默认移动赋值。如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
2.2 default
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 显式指定移动构造生成。
2.3 delete
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 是禁止生成默认函数的关键字。当类显式删除了拷贝构造函数时,编译器不会自动生成移动构造函数(即使没有显式删除移动构造函数)。代码中没有显式定义移动构造函数,且隐式移动构造函数被禁用,因此无法完成移动初始化。
注意: 移动构造函数的核心目的是高效转移资源所有权(如动态内存、文件句柄等),而拷贝构造函数是创建独立副本。如果一个类禁用了拷贝构造函数,通常意味着资源不可复制或防止意外拷贝。此时若编译器仍自动生成移动构造函数,可能会破坏设计意图。
3. 可变参数模板
3.1 概念
其实可变模板参数早在 C 语言就有了,比如 printf("%d,%d,%d", x, y, z),后面的参数个数可以自己控制,这就是可变参数。回到 C++ 模板定义:
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 到任意个模板参数。
3.2 获取个数
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;
}
3.3 展开参数包
想要用 for 循环遍历展开是不可行的,编译器不支持。这里主要有两种展开方法。
3.3.1 递归函数
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,再递归处理剩余参数,确保参数按传入顺序输出。
3.3.2 逗号表达式
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 返回参数类型,参数包可能包含不同类型(如 int, char),仍会导致类型不匹配。每个元素必须是 int 类型,因此需要用 0 作为统一的返回值,保证初始化的数组元素都为相同类型。
3.4 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 是先构造再移动构造,移动构造消耗很小,实际影响有限。
3.5 可变参数模板的实际应用
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 既能传日期类对象,又能传日期类对象的参数包。参数包一路往下传,直接去构造或者拷贝构造节点中日期类对象。
4. 包装器
4.1 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 也叫作适配器,本质是一个类模板。
template<class T> function;
template<class Ret, class... Args>
class function<Ret(Args...)>;
Ret:被调用函数的返回类型
Args...:被调用函数的形参
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 只有一份)。
4.2 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
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,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