C++雾中风景9:emplace_back与可变长模板
C++11的版本在vector容器添加了 emplace_back方法 ,相对于原先的push_back方法能够在一定程度上提升vector容器的表现性能。所以我们从STL源码角度来切入,看看这两种方法有什么样的区别,新引进的方法又有什么可学习参考之处。
1.emplace_back的用法
emplace_back方法 最大的改进就在与可以利用类本身的构造函数直接在内存之中构建对象,而不需要调用类的 拷贝构造函数 与 移动构造函数 。
举个栗子,假设如下定义了一个时间类 time ,该类同时定义了 拷贝构造函数 与 移动构造函数 :
class time {
private:
int hour;
int minute;
int second;
public:
time(int h, int m, int s) :hour(h), minute(m), second(s) {
time(const time& t) :hour(t.hour), minute(t.minute), second(t.second) {
cout << "copy" << endl;
time(const time&& t) noexcept:hour(t.hour),minute(t.minute),second(t.second) {
cout << "move" << endl;
};
在 main 方法之中执行下面的代码逻辑:
int main()
vector<time> tlist;
time t(1, 2, 3);
tlist.emplace_back(t);
tlist.emplace_back(2, 3, 4); //直接调用了time的构造函数在vector的内存之中建立起新的对象
getchar();
}
执行结果:
copy
move (这次拷贝构造函数的调用是因为vector本身的扩容,也就是移动之前的已经容纳的time对象)
由上述代码我们看到time对象可以直接利用 emplace_back 方法在 vector 上构造对象,通过这样的方式来减少不必要的内存操作。( 省去了拷贝构造的环节 )。同样的在 main 之中执行下面的代码逻辑:
int main()
vector<time> tlist;
time t(1, 2, 3);
tlist.emplace_back(move(t)); //调用move函数使time对象成为右值,可以利用移动构造函数来拷贝对象
tlist.emplace_back(2, 3, 4); //直接调用了time的构造函数在vector的内存之中建立起新的对象
getchar();
}
执行结果:
move
move (这次拷贝构造函数的调用是因为vector本身的扩容,也就是移动之前的已经容纳的time对象)
通过这样的方式也减少不必要的内存操作。( 省去了移动构造的环节 )。所以这就是为什么在C++11之后提倡大家 使用emplace_back来代替旧代码之中的push_back函数。 如下面的代码所示,在push_back底层也是调用了emplace_back来实现对应的操作流程:
void push_back(const _Ty& _Val) {
emplace_back(_Val);
void push_back(_Ty&& _Val) {
emplace_back(_STD move(_Val));
}
2.emplace_back的实现
源码面前,了无秘密 ,接下来跟随笔者直接来看看emplace_back的源代码,来引出我们今天的主题:
public:
template<class... _Valty>
decltype(auto) emplace_back(_Valty&&... _Val)
{ // insert by perfectly forwarding into element at end, provide strong guarantee
if (_Has_unused_capacity())
_Emplace_back_with_unused_capacity(_STD forward<_Valty>(_Val)...);
{ // reallocate
const size_type _Oldsize = size();
if (_Oldsize == max_size())
_Xlength();
const size_type _Newsize = _Oldsize + 1;
const size_type _Newcapacity = _Calculate_growth(_Newsize);
bool _Emplaced = false;
const pointer _Newvec = this->_Getal().allocate(_Newcapacity);
_Alty& _Al = this->_Getal();
_TRY_BEGIN
_Alty_traits::construct(_Al, _Unfancy(_Newvec + _Oldsize), _STD forward<_Valty>(_Val)...);
_Emplaced = true;
_Umove_if_noexcept(this->_Myfirst(), this->_Mylast(), _Newvec);
_CATCH_ALL
if (_Emplaced)
_Alty_traits::destroy(_Al, _Unfancy(_Newvec + _Oldsize));
_Al.deallocate(_Newvec, _Newcapacity);
_RERAISE;
_CATCH_END
_Change_array(_Newvec, _Newsize, _Newcapacity);
#if _HAS_CXX17
return (this->_Mylast()[-1]);
#endif /* _HAS_CXX17 */
}
通过上述代码可以看到,emplace_back的流程逻辑很简单。先检查vector的容量,不够的话就扩容,之后便通过 _Alty_traits::construct 来创建对象。而最终利用强制类似装换的指针来指向容器类之中对应类的构造函数,并且利用 可变长模板 将构造函数所需要的内容传递过去构造新的对象。
template<class _Objty,
class... _Types>
static void construct(_Alloc&, _Objty * const _Ptr, _Types&&... _Args)
{ // construct _Objty(_Types...) at _Ptr
::new (const_cast<void *>(static_cast<const volatile void *>(_Ptr)))
_Objty(_STD forward<_Types>(_Args)...);
}
emplace_back这里最为巧妙的部分就是利用 可变长模板 实现了,任意传参的对象构造。可变长模板是C++11新引进的特性,接下来我们来详细看看可变长模板是如何来使用,来实现任意长度的参数呢?
3.可变长模板与函数式编程
首先,我们先看看,可变长模板的定义:
template <class... T>
void f(T... args);
通过template来声明参数包args,这个参数包中可以包含0到任意个参数,并且作为函数参数调用。之后我们便可以在函数之中将参数包展开成一个一个独立的参数。
假设我们有如下需求,需要定义一个 max_num函数 来求出一组任意参数数字的最大值,在C++11之前的版本或许需要这样去定义这个函数,也就是说我们需要一个参数来指定对应参数的个数,并且这个过程之中存在参数的类型不一致的潜在风险,并不能在编译期进行反馈( 不能在编译期进行对于动态语言来说根本不是什么大不了的问题,囧rz ):
int max_num(int count, ...)
va_list ap;
va_start(ap, count);
int ans = va_arg(ap, int);
for (int i = 1; i < count; ++i)
int num = va_arg(ap, int);
ans = max(ans, num);
va_end(ap);
return ans;
}
而利用可变长模板,我们可以很优雅地通过以下的代码来实现一个这样的函数:
template<typename t1,typename ...t2> t1 max_num(t1 num, t2 ...args) {