C++11可变参数模板、emplace系列接口、包装器

目录

可变参数模板

基本语法及原理

参数包的展开方式

递归展开参数包

STL容器中的emplace相关接口函数

包装器

function包装器

function包装器介绍

function包装器的意义

bind包装器

bind包装器介绍

bind包装器绑定固定参数


可变参数模板

基本语法及原理

C++11支持可变参数模板,也就是说支持可变参数数量的函数模板和类模板,可变数目的参数被称 为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函 数参数。

我们用省略号来指出⼀个模板参数或函数参数的表示⼀个包,在模板参数列表中,class...或 typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...指出 接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板 ⼀样,每个参数实例化时遵循引用折叠规则。

可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。

下面我们可以使用sizeof...运算符去计算参数包中参数的个数。

template <class ...Args>
void Print(Args&&... args)
{
	cout << sizeof...(args) << endl;//获取参数包中参数的个数
}

int main()
{
	double x = 2.2;

	Print(); // 包里有0个参数
	Print(1); // 包里有1个参数
	Print(1, string("xxxxx")); // 包里有2个参数
	Print(1.1, string("xxxxx"), x); // 包里有3个参数

	return 0;
}

// 编译本质这⾥会结合引⽤折叠规则实例化出以下四个函数

void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);

但是我们无法直接获取参数包中的每个参数,只能通过展开参数包的方式来获取,这是使用可变参数模板的一个主要特点,也是最大的难点。

特别注意,语法并不支持使用args[i]的方式来获取参数包中的参数。比如:

template <class ...Args>
void Print(Args... args)
{
	// 可变参数模板编译时解析
	// 下面是运行获取和解析,所以不支持这样用
	cout << sizeof...(args) << endl;
	for (size_t i = 0; i < sizeof...(args); i++)
	{
		cout << args[i] << " ";
	}
}

int main()
{
	double x = 2.2;
	Print(1, string("xxxxx"), x); // 包里有3个参数

	return 0;
}

参数包的展开方式

递归展开参数包

递归展开参数包的方式如下:

  • 给函数模板增加一个模板参数,这样就可以从接收到的参数包中分离出一个参数出来。
  • 在函数模板中递归调用该函数模板,调用时传入剩下的参数包。
  • 如此递归下去,每次分离出参数包中的一个参数,直到参数包中的所有参数都被取出来。

比如我们要打印调用函数时传入的各个参数,那么函数模板可以这样编写:
 

template <class T, class ...Args>
void ShowList(T x, Args... args)
{
	cout << x << " ";
	// args是N个参数的参数包
	// 调用ShowList,参数包的第一个传给x,剩下N-1传给第二个参数包
	ShowList(args...);
}

那么在递归中,我们怎么最后怎么终止递归呢?其实并不难,我们可以在刚才的基础上,再编写一个无参的递归终止函数,该函数的函数名与展开函数的函数名相同。如下:

//编译递归推导的包扩展
void ShowList()
{
	// 编译器时递归的终止条件,参数包是0个时,直接匹配这个函数
	cout << endl;
}

这样一来,当递归调用ShowList函数模板时,如果传入的参数包中参数的个数为0,那么就会匹配到这个无参的递归终止函数,这样就结束了递归。

但如果外部调用ShowList函数时就没有传入参数,那么就会直接匹配到无参的递归终止函数。
而我们本意是想让外部调用ShowList函数时匹配的都是函数模板,并不是让外部调用时直接匹配到这个递归终止函数。
鉴于此,我们可以提供一个供外部调用的函数Print(),它可以接受任意数量、任意类型的参数

 //编译递归推导的包扩展
void ShowList()
{
	// 编译器时递归的终止条件,参数包是0个时,直接匹配这个函数
	cout << endl;
}
template <class T, class ...Args>
void ShowList(T x, Args... args)
{
	cout << x << " ";
	// args是N个参数的参数包
	// 调用ShowList,参数包的第一个传给x,剩下N-1传给第二个参数包
	ShowList(args...);
}

// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args)
{
	ShowList(args...);
}

为空就终止递归,为什么不能在递归内部判断为空来终止递归?

比如下面代码这样

template <class T, class ...Args>
void ShowList(T x, Args... args)
{
	if (sizeof...(args) == 0)
		return;
	cout << x << " ";
	// args是N个参数的参数包
	// 调用ShowList,参数包的第一个传给x,剩下N-1传给第二个参数包
	ShowList(args...);
}

这种方式是不可行的,原因如下:

编译时递归 vs 运行时递归

C++ 的模板递归是编译时展开​的,编译器必须在编译期间生成所有可能的函数实例。即使 if (sizeof...(args) == 0)在运行时判断为 true,编译器仍然需要实例化所有可能的递归调用路径,包括 ShowList(args...)的所有可能版本。

args为空时,ShowList(args...)会尝试调用 ShowList(),但此时没有匹配的函数重载​(除非你显式定义了一个无参数的 ShowList()),因此 编译失败。

STL容器中的emplace相关接口函数

emplace版本的插入接口

C++11标准给STL中的容器增加emplace版本的插入接口,比如list容器的push_front、push_back和insert函数,都增加了对应的emplace_front、emplace_back和emplace函数。如下:

这些emplace版本的插入接口支持模板的可变参数,比如list容器的emplace_back函数的声明如下:

注:&&是万能模板而非右值引用

emplace系列接口的使用方式

emplace系列接口的使用方式与容器原有的插入接口的使用方式类似,但又有一些不同之处。

以list容器的emplace_back和push_back为例:

  • 调用push_back函数插入元素时,可以传入左值对象或者右值对象,也可以使用列表进行初始化。
  • 调用emplace_back函数插入元素时,也可以传入左值对象或者右值对象,但不可以使用列表进行初始化。
  • 除此之外,emplace系列接口最大的特点就是,插入元素时可以传入用于构造元素的参数包。
     

代码如下

int main()
{
	lzg::list<lzg::string> lt;
	// 传左值,跟push_back一样,走拷贝构造
	lzg::string s1("111111111111");
	lt.emplace_back(s1);
	cout << "*********************************" << endl;

	// 右值,跟push_back一样,走移动构造
	lt.emplace_back(move(s1));
	cout << "*********************************" << endl;

	// 直接把构造string参数包往下传,直接用string参数包构造string
	// 这里达到的效果是push_back做不到的
    //push_back只能走隐式类型转换变成构造+移动构造
	lt.emplace_back("111111111111");
	cout << "*********************************" << endl << endl << endl;

	lzg::list<pair<lzg::string, int>> lt1;
	// 跟push_back一样
	// 构造pair + 拷贝/移动构造pair到list的节点中data上
	pair<lzg::string, int> kv("苹果", 1);
	lt1.emplace_back(kv);
	cout << "*********************************" << endl;

	// 跟push_back一样
	lt1.emplace_back(move(kv));
	cout << "*********************************" << endl;

	// 直接把构造pair参数包往下传,直接用pair参数包构造pair
    //const char*+int
	// 这里达到的效果是push_back做不到的
    //push_back对于多参数的隐式类型转换要用花括号
	//lt1.push_back({"苹果", 1 });
	lt1.emplace_back("苹果", 1);
	cout << "*********************************" << endl;

	return 0;
}

emplace接口的意义:

  • emplace系列接口最大的特点就是支持传入参数包,用这些参数包直接构造出对象,这样就能减少一次拷贝,这就是为什么有人说emplace系列接口更高效的原因。
  • 但emplace系列接口并不是在所有场景下都比原有的插入接口高效,如果传入的是左值对象或右值对象,那么emplace系列接口的效率其实和原有的插入接口的效率是差不多的(编译器优化)。
  • emplace系列接口真正高效的情况是传入参数包的时候,直接通过参数包构造出对象,避免了中途的一次拷贝。

包装器

function包装器

function包装器介绍

function包装器

function是一种函数包装器,也叫做适配器。它可以对可调用对象进行包装,C++中的function本质就是一个类模板。

function类模板的原型如下:

template <class T> function;     // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
  • std::function是一个类模板,也是一个包装器。std::function的实例对象可以包装存储其他的可调用对象,包括函数指针、仿函数、lambda、bind表达式等,存储的可调用对象被称为 std::function的目标。若 std::function不含目标,则称它为空。调用空的 std::function会导致抛出 std::bad_function_call异常。
  • 以上是 function的原型,它被定义在 <functional>头文件中。std::function - cppreference.comfunction的官方文档链接。
  • 函数指针、仿函数、lambda 等可调用对象的类型各不相同,std::function的优势就是统一类型,对它们都可以进行包装。这样在很多地方就方便声明可调用对象的类型。
class Plus
{
public:
	Plus(int n = 10)
		:_n(n)
	{}
	static int plusi(int a, int b)
	{
		return a + b;
	}
	double plusd(double a, double b)
	{
		return (a + b) * _n;
	}
private:
	int _n;
};

int main()
{
	int(*pf)(int, int) = f;

	// 包装各种可调用对象
	function<int(int, int)> f1 = f;
	function<int(int, int)> f2 = Functor();
	function<int(int, int)> f3 = [](int a, int b) {return a + b; };

	cout << f1(1, 1) << endl;
	cout << f2(1, 1) << endl;
	cout << f3(1, 1) << endl;

	// 包装静态成员函数
	// 成员函数要指定类域并且前面加&才能获取地址
	function<int(int, int)> f4 = &Plus::plusi;
	cout << f4(1, 1) << endl;

	function<double(Plus*, double, double)> f5 = &Plus::plusd;
	Plus ps;
	cout << f5(&ps,1.1, 1.1) << endl;

	function<double(Plus&&, double, double)> f6 = &Plus::plusd;
	cout << f6(Plus(), 1.1, 1.1) << endl;

	return 0;
}
function包装器的意义
  • 将可调用对象的类型进行统一,便于我们对其进行统一化管理。
  • 包装后明确了可调用对象的返回值和形参类型,更加方便使用者使用。

bind包装器

bind包装器介绍

bind包装器

bind也是一种函数包装器,也叫做适配器。它可以接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表,C++中的bind本质是一个函数模板。

bind函数模板的原型如下:

template <class Fn, class... Args>
/* unspecified */ bind(Fn&& fn, Args&&... args);
template <class Ret, class Fn, class... Args>
/* unspecified */ bind(Fn&& fn, Args&&... args);
  • std::bind是一个函数模板,也是一个可调用对象的包装器,可以把它看作一个函数适配器,对接收的 fn可调用对象进行处理后返回一个新的可调用对象。
  • std::bind可以用来调整参数个数和参数顺序。std::bind同样定义在 <functional>头文件中。

调用 std::bind的一般形式:

auto newCallable = std::bind(callable, arg_list);
  • 其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的 callable的参数。当我们调用 newCallable时,newCallable会调用 callable,并传递给它 arg_list中的参数。
  • std::bindarg_list中的参数可能包含形如 _n的名字,其中 n是一个整数。这些参数是占位符,表示 newCallable的参数,它们占据了传递给 newCallable的参数的位置。数值 n表示生成的可调用对象中参数的位置:_1表示 newCallable的第一个参数,_2表示第二个参数,以此类推。
  • _1_2_3……这些占位符定义在 std::placeholders命名空间中,使用时需要指定该命名空间。
bind包装器绑定固定参数

无意义的绑定

下面这种绑定就是无意义的绑定:

int Plus(int a, int b)
{
	return a + b;
}
int main()
{
	//无意义的绑定
	function<int(int, int)> func = bind(Plus, placeholders::_1, placeholders::_2);
	cout << func(1, 2) << endl; //3
	return 0;
}

绑定时第一个参数传入函数指针这个可调用对象,但后续传入的要绑定的参数列表依次是placeholders::_1和placeholders::_2,表示后续调用新生成的可调用对象时,传入的第一个参数传给placeholders::_1,传入的第二个参数传给placeholders::_2。此时绑定后生成的新的可调用对象的传参方式,和原来没有绑定的可调用对象是一样的,所以说这是一个无意义的绑定。

绑定固定参数

如果想把Plus函数的第二个参数固定绑定为10,可以在绑定时将参数列表的placeholders::_2设置为10。比如:

int Plus(int a, int b)
{
	return a + b;
}
int main()
{
	//绑定固定参数
	function<int(int)> func = bind(Plus, placeholders::_1, 10);
	cout << func(2) << endl; //12
	return 0;
}

此时调用绑定后新生成的可调用对象时就只需要传入一个参数,它会将该值与10相加后的结果进行返回。

<think>首先,用户的问题是关于在C++中使用`emplace_back`方法并传入三个参数的用法。用户指定了系统级指令,所以我必须遵守这些规则: 1. 所有行内数学表达式使用`$...$`格式。 2. 独立公式使用`$$...$$`格式并单独成段。 3. 确保LaTeX语法正确。 4. 使用中文回答。 5. 生成相关问题。 6. 回答中引用的段落末尾自然地添加引用标识,例如[^1]。 用户还提供了引用内容,我需要参考这些引用。引用包括: - [^1]: 关于setcode回调函数和`emplace_back`的代码片段。 - [^2]: 一个C++动态规划示例代码。 - [^3]: CLion使用技巧。 用户的问题是关于`emplace_back`的用法,所以我应该聚焦于这个主题。 关键点:`emplace_back`是C++中vector的一个方法,它允许直接在容器中构造元素,而不需要创建临时对象。它接受参数列表,这些参数被转发给元素的构造函数。 用户提到“传入三个参数”,所以我需要解释如何用三个参数使用`emplace_back`。 结构回答: 1. **解释`emplace_back`**:简要介绍`emplace_back`的作用和优势。 2. **用法示例**:提供一个代码示例,展示如何用三个参数调用`emplace_back`。选择一个合适的类或结构体,因为`emplace_back`需要元素类型有对应的构造函数。 - 例如,定义一个简单的类如`Point`,有x、y、z三个成员。 - 然后使用`emplace_back`来添加元素。 3. **引用相关内容**:用户提供的引用[^1]中提到了`emplace_back`,所以我应该在相关部分添加引用标识。 4. **生成相关问题**:在回答结束时,添加§§相关问题§§部分,列出几个相关问题。 确保回答使用中文。 代码示例:用C++写一个简单的例子。定义一个类,比如: ```cpp class MyClass { public: MyClass(int a, int b, int c) : x(a), y(b), z(c) {} private: int x, y, z; }; int main() { std::vector<MyClass> vec; vec.emplace_back(1, 2, 3); // 使用三个参数 return 0; } ``` 在解释中,强调`emplace_back`避免了创建临时对象,提高了效率。 参考引用: - 引用[^1]中提到了`actions.emplace_back(create_setcode(...))`,这类似于`emplace_back`的用法。我可以引用这个作为示例。 - 其他引用可能不直接相关,但可以作为一般参考。 生成相关问题: 1. `emplace_back`与`push_back`有什么区别? 2. 如何在C++中优化vector的性能? 3. `emplace_back`在哪些情况下可能导致问题? 确保回答结构清晰,逐步解决问题。 最终回答结构: - 开头解释`emplace_back`。 - 提供代码示例。 - 讨论注意事项。 - 添加相关问题。</think>### C++ 中 `emplace_back` 方法使用三个参数的用法 在 C++ 中,`std::vector` 的 `emplace_back` 方法允许直接在容器中构造元素,避免了创建临时对象的开销,从而提高效率和性能。它通过完美转发(perfect forwarding)将参数直接传递给元素的构造函数。当您需要传入三个参数时,`emplace_back` 会将这些参数转发给元素类型的构造函数,从而在 vector 的末尾就地构造一个新元素。以下我将逐步解释其用法、示例和注意事项。 #### 1. **`emplace_back` 的基本原理** - `emplace_back` 是 C++11 引入的特性,它接受可变参数模板(variadic template),允许传递任意数量的参数。 - 与 `push_back` 不同,`emplace_back` 直接在 vector 的内存中构造对象,减少了拷贝或移动操作的开销,尤其适用于复杂对象或资源密集型类[^1]。 - 数学上,这类似于就地构造的优化:$ \text{效率} = \frac{\text{直接构造}}{\text{临时对象 + 拷贝}} $,当元素构造函数复杂时,优势更明显。 #### 2. **使用三个参数的代码示例** 假设我们有一个自定义类 `MyClass`,其构造函数接受三个参数(例如整数、字符串和布尔值)。以下是完整示例: ```cpp #include <iostream> #include <vector> #include <string> // 定义一个自定义类,构造函数接受三个参数 class MyClass { public: MyClass(int id, std::string name, bool active) : id_(id), name_(std::move(name)), active_(active) { std::cout << "对象已构造: " << id_ << ", " << name_ << ", " << active_ << std::endl; } private: int id_; std::string name_; bool active_; }; int main() { std::vector<MyClass> myVec; // 使用 emplace_back 传入三个参数 myVec.emplace_back(1, "Alice", true); // 直接在 vector 中构造对象 myVec.emplace_back(2, "Bob", false); return 0; } ``` - **关键点解释**: - `emplace_back(1, "Alice", true)`:这三个参数被直接转发给 `MyClass` 的构造函数,无需创建临时 `MyClass` 对象。 - 输出结果:运行程序后,控制台会打印构造信息,如 `对象已构造: 1, Alice, 1`,证明对象在 vector 内部被构造。 - 效率优势:避免了 `push_back` 可能导致的额外拷贝(例如 `push_back(MyClass(1, "Alice", true))` 会先构造临时对象再拷贝)。 #### 3. **注意事项和最佳实践** - **参数类型匹配**:参数必须与元素类型的构造函数严格匹配。例如,如果构造函数是 `MyClass(int, const char*, bool)`,则 `emplace_back(1, "Alice", true)` 有效;但如果参数类型不兼容(如传递浮点数代替整数),编译器会报错。 - **性能优化**:`emplace_back` 在元素构造复杂时效率更高,但简单类型(如内置类型)可能差异不大。 - **引用和移动语义**:使用 `std::move` 处理字符串等可移动对象,避免不必要的拷贝(如示例中的 `std::move(name)`)。 - **错误处理**:如果 vector 内存不足,`emplace_back` 可能抛出 `std::bad_alloc` 异常,建议使用 `reserve()` 预分配内存。 - 参考引用[^1]中的 `actions.emplace_back(create_setcode(...))`,类似地展示了如何通过 `emplace_back` 高效构造对象。 #### 4. **与 `push_back` 的对比** - `emplace_back` 更适合构造开销大的对象,而 `push_back` 适用于已有对象的情况。 - 示例对比: ```cpp // 使用 push_back(需要临时对象) myVec.push_back(MyClass(3, "Charlie", true)); // 额外拷贝开销 // 使用 emplace_back(无临时对象) myVec.emplace_back(3, "Charlie", true); // 更高效 ``` - 数学优化:在时间复杂度上,$ \text{emplace\_back} = O(1) \text{ 摊销} $,但避免了 $O(n)$ 的拷贝开销。
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值