C++11中可變模板參數(shù)的實現(xiàn)
C++11的新特性可變參數(shù)模板能夠讓您創(chuàng)建可以接受可變參數(shù)的函數(shù)模板和類模板,相比 C++98/03,類模版和函數(shù)模版中只能含固定數(shù)量的模版參數(shù),可變模版參數(shù)無疑是一個巨大的改 進。
像之前學(xué)習(xí)的printf就是一個函數(shù)參數(shù)的可變參數(shù),它可以接收多個任意類型,但它們只函數(shù)參數(shù)的可變參數(shù),并不是模板的可變參數(shù)
printf的使用方法:
int printf( const char *format , ... );
本博客講解的是函數(shù)模板的可變參數(shù),不會涉及到類模板的可變參數(shù)
可變模板的定義方式
函數(shù)的可變參數(shù)模板定義方式如下:
template<class ...Args> //Args全稱:arguments
返回類型 函數(shù)名(Args... args)
{
//函數(shù)體
}下面就是一個基本可變參數(shù)的函數(shù)模板
template <class ...Args>
void ShowList(Args... args)
{}Args:是一個可變模板參數(shù)包
args:是一個函數(shù)形參參數(shù)包
說明一下:
模板參數(shù)Args前面有省略號,代表它是一個可變模板參數(shù),我們將帶省略號的參數(shù)稱為 “參數(shù)包”,這個參數(shù)包中可以包含0到任意個模板參數(shù),args則是一個函數(shù)形參參數(shù)包
現(xiàn)在我們可以向這個函數(shù)中傳入多個不同的類型,并且可以通過sizeof算出參數(shù)包的參數(shù)個數(shù)
以下例代碼為例:
template<class ...Args>
void ShowList(Args... args)
{
cout << sizeof...(args) << endl;
}
int main()
{
ShowList(1);
ShowList(1, 2);
ShowList(1, 2, string("dict"));
map<string, int> m1;
ShowList(1, 2, 3, m1);
return 0;
}
我們無法直接獲取參數(shù)包args中的每個參數(shù)的, 只能通過展開參數(shù)包的方式來獲取參數(shù)包中的每個參數(shù),這是使用可變模版參數(shù)的一個主要特點,也是最大的難點,即如何展開可變模版參數(shù)。
由于C++11語法不支持使用args[i]這樣方式獲取可變q參數(shù),所以我們的用一些奇招來一一獲取參數(shù)包的值。
錯誤示例:
template<class ...Args>
void ShowList(Args... args)
{
//error
for (int i = 0; i < sizeof...(args); ++i)
{
cout << args[i] << endl;
}
}參數(shù)包的展開方式
遞歸的方式展開參數(shù)包
方式如下:
1.給函數(shù)模板新增一個參數(shù),這樣就可以從接收到的參數(shù)包分離出來一個參數(shù)
2.在函數(shù)模板中進行遞歸,不斷的分離參數(shù)包中的參數(shù)
3.直到接收到最后一個參數(shù)結(jié)束
結(jié)束條件;
->1. 可以創(chuàng)建一個無參的函數(shù)來終止遞歸:當(dāng)參數(shù)包中的參數(shù)為0時會調(diào)用該函數(shù)終止循環(huán)
void _ShowList()
{
cout << endl;
}
template<class T, class ...Args>
void _ShowList(T value, Args... args)
{
cout << value << ' ';
_ShowList(args...);
}
int main()
{
_ShowList(1, 2, string("dict"));
return 0;
}
->2. 可以創(chuàng)建一個參數(shù)的函數(shù)來終止遞歸:當(dāng)參數(shù)包中的參數(shù)為1時會調(diào)用該函數(shù)終止循環(huán)
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, 2, string("dict"));
return 0;
}
但是使用該方法有一個弊端:我們在調(diào)用ShowList函數(shù)時必須至少傳入一個參數(shù),否則就會報錯,因為此時無論是調(diào)用遞歸終止函數(shù)還是展開函數(shù),都需要至少傳入一個參數(shù)
使用sizeof...(args)算出參數(shù)個數(shù)的特性,利用它的特性做一個遞歸結(jié)束條件可以嗎?不行!
template<class T, class ...Args>
void ShowList(T value, Args... args)
{
cout << value << ' ';
if (sizeof...(args))
{
return;
}
ShowList(args...);
}函數(shù)模板并不能調(diào)用,函數(shù)模板需要在編譯時根據(jù)傳入的實參類型進行推演,生成對應(yīng)的函數(shù),這個生成的函數(shù)才能夠被調(diào)用。
而這個推演過程是在編譯時進行的,當(dāng)推演到參數(shù)包args中參數(shù)個數(shù)為0時,還需要將當(dāng)前函數(shù)推演完畢,這時就會繼續(xù)推演傳入0個參數(shù)時的ShowList函數(shù),此時就會產(chǎn)生報錯,因為ShowList函數(shù)要求至少傳入一個參數(shù)。
這里編寫的if判斷是在代碼編譯結(jié)束后,運行代碼時才會所走的邏輯,也就是運行時邏輯,而函數(shù)模板的推演是一個編譯時邏輯。
還有一種特殊的方式,該方法比較抽象,就是使用逗號表達式展開參數(shù)包
->3. 逗號表達式展開參數(shù)包
template<class T>
void CPPprint(const T& value)
{
cout << value << ' ';
}
template<class ...Args>
void ShowList(Args... args)
{
int array[] = {( CPPprint(args), 0)...};
cout << endl;
}當(dāng)我們在數(shù)組中不標(biāo)注元素個數(shù)時,編譯器會幫我們自動推導(dǎo)元素個數(shù),這時它會幫我們展開參數(shù)包
如下:
int array[] = {( CPPprint(args), 0), CPPprint(args), 0), CPPprint(args), 0), CPPprint(args), 0)};在調(diào)用CPPprint函數(shù)的同時,利用逗號運算符的特性進行對數(shù)組的初始化
其實也可以不使用逗號運算符完成該操作
template<class T>
int CPPprint(const T& value)
{
cout << value << ' ';
return 0;
}
template<class ...Args>
void ShowList(Args... args)
{
int array[] = { (CPPprint(args))... };
cout << endl;
}將被調(diào)用的函數(shù)設(shè)置一個返回值,調(diào)用之后返回0,這樣就可以在編譯器展開參數(shù)包調(diào)用函數(shù)時,通過返回值初始化
STL中的emplace相關(guān)接口函數(shù)
以便大家更好的理解emplace,先給大家看一段代碼,可變模板參數(shù)的使用場景:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Data()~構(gòu)造函數(shù)" << endl;
}
Date(const Date& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date()~拷貝構(gòu)造" << endl;
}
private:
int _year;
int _month;
int _day;
};
template<class ...Args>
Date* Init(Args&&... args)
{
Date* ret = new Date(args...);
return ret;
}
int main()
{
Date* p1 = Init();
Date* p2 = Init(2024);
Date* p3 = Init(2024, 11);
Date* p4 = Init(2024, 11, 12);
Date d1(2, 3, 3);
Date* p5 = Init(d1);
return 0;
}
我們通過將參數(shù)傳入?yún)?shù)包在編譯期間通過將參數(shù)包展開的操作進行對象的構(gòu)造
STL容器中emplace相關(guān)插入接口函數(shù)
C++11標(biāo)準(zhǔn)STL中的容器增加emplace版本的插入接口,比如list容器的push_front,push_back和insert函數(shù),都增加了對應(yīng)的emplace_front,emplace_back,emplace函數(shù)。如下:



emplace接口全部都是使用的可變參數(shù)模板
注意:兩個&&是萬能引用并不是右值引用
對比list中的push_back和emplace_back,對于emplace系列接口而言,它的主要優(yōu)勢就是直接在容器內(nèi)部構(gòu)造元素可以結(jié)合我上面給的場景進行理解,而不是構(gòu)造一個臨時對象在復(fù)制或移動到容器中可以有效的避免拷貝和移動操作
以emplace和push_back為例:
調(diào)用push_back函數(shù)插入元素時,可以傳入左值對象或者右值對象,也可以使用列表初始化
調(diào)用emplace時可以傳左值對象或者右值對象,但是不能使用列表初始化,emplace系列最大的特點就是,插入元素時可以傳入用于構(gòu)造元素的參數(shù)包
比如:
int main()
{
list<pair<nxbw::string, int>> mylist;
pair<nxbw::string, int> kv("nxbw", 10);
mylist.emplace_back(kv); //傳左值
mylist.emplace_back(make_pair("nxbw", 10)); //傳右值
mylist.emplace_back("nxbw", 10); //傳參數(shù)包
mylist.push_back(kv); //傳左值
mylist.push_back(make_pair("nxbw", 10)); //傳右值
mylist.push_back({ "nxbw", 10 }); //使用列表初始化
return 0;
}原地構(gòu)造:使用emplace,你可以提供構(gòu)造元素所需的參數(shù),容器會直接在emplace接口的實現(xiàn)中構(gòu)造該對象
emplace系列接口的工作流程
emplace系列接口的工作流程如下:
- 先通過空間配置器為新結(jié)點獲取一塊內(nèi)存空間,注意這里只會開辟空間,不會自動調(diào)用構(gòu)造函數(shù)對這塊空間進行初始化。
- 然后調(diào)用allocator_traits::construct函數(shù)對這塊空間進行初始化,調(diào)用該函數(shù)時會傳入這塊空間的地址和用戶傳入的參數(shù)(需要經(jīng)過完美轉(zhuǎn)發(fā))。
- 在allocator_traits::construct函數(shù)中會使用定位new表達式,顯示調(diào)用構(gòu)造函數(shù)對這塊空間進行初始化,調(diào)用構(gòu)造函數(shù)時會傳入用戶傳入的參數(shù)(需要經(jīng)過完美轉(zhuǎn)發(fā))。
- 將初始化好的新結(jié)點插入到對應(yīng)的數(shù)據(jù)結(jié)構(gòu)當(dāng)中,比如list容器就是將新結(jié)點插入到底層的雙鏈表中。
emplace系列接口的意義
由于emplace系列接口的可變模板參數(shù)的類型都是萬能引用,因此既可以接收左值對象,也可以接收右值對象,還可以接收參數(shù)包。
- 如果調(diào)用emplace系列接口時傳入的是左值對象,那么首先需要先在此之前調(diào)用構(gòu)造函數(shù)實例化出一個左值對象,最終在使用定位new表達式調(diào)用構(gòu)造函數(shù)對空間進行初始化時,會匹配到拷貝構(gòu)造函數(shù)。
- 如果調(diào)用emplace系列接口時傳入的是右值對象,那么就需要在此之前調(diào)用構(gòu)造函數(shù)實例化出一個右值對象,最終在使用定位new表達式調(diào)用構(gòu)造函數(shù)對空間進行初始化時,就會匹配到移動構(gòu)造函數(shù)。
- 如果調(diào)用emplace系列接口時傳入的是參數(shù)包,那就可以直接調(diào)用函數(shù)進行插入,并且最終在使用定位new表達式調(diào)用構(gòu)造函數(shù)對空間進行初始化時,匹配到的是構(gòu)造函數(shù)。
總結(jié)一下:
- 傳入左值對象,需要調(diào)用構(gòu)造函數(shù)+拷貝構(gòu)造函數(shù)。
- 傳入右值對象,需要調(diào)用構(gòu)造函數(shù)+移動構(gòu)造函數(shù)。
- 傳入?yún)?shù)包,只需要調(diào)用構(gòu)造函數(shù)。
當(dāng)然,這里的前提是容器中存儲的元素所對應(yīng)的類,是一個需要深拷貝的類,并且該類實現(xiàn)了移動構(gòu)造函數(shù)。否則在調(diào)用emplace系列接口時,傳入左值對象和傳入右值對象的效果都是一樣的,都需要調(diào)用一次構(gòu)造函數(shù)和一次拷貝構(gòu)造函數(shù)。
實際emplace系列接口的一部分功能和原有各個容器插入接口是重疊的,因為容器原有的push_back、push_front和insert函數(shù)也提供了右值引用版本的接口,如果調(diào)用這些接口時如果傳入的是右值對象,那么最終也是會調(diào)用對應(yīng)的移動構(gòu)造函數(shù)進行資源的移動的。
emplace接口的意義:
emplace系列接口最大的特點就是支持傳入?yún)?shù)包,用這些參數(shù)包直接構(gòu)造出對象,這樣就能減少一次拷貝,這就是為什么有人說emplace系列接口更高效的原因。
但emplace系列接口并不是在所有場景下都比原有的插入接口高效,如果傳入的是左值對象或右值對象,那么emplace系列接口的效率其實和原有的插入接口的效率是一樣的。
emplace系列接口真正高效的情況是傳入?yún)?shù)包的時候,直接通過參數(shù)包構(gòu)造出對象,避免了中途的一次拷貝。
通過下面的場景我們來驗證一下:
namespace nxbw
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷貝構(gòu)造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷貝" << endl;
string tmp(s._str);
swap(tmp);
}
// 賦值重載
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷貝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移動構(gòu)造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移動語義" << endl;
swap(s);
}
// 移動賦值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移動語義" << endl;
swap(s);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做標(biāo)識的\0
};
}int main()
{
list<pair<nxbw::string, int>> mylist;
pair<nxbw::string, int> kv("nxbw", 10); //構(gòu)造
mylist.emplace_back(kv); //傳左值,
mylist.emplace_back(pair<nxbw::string, int>("nxbw", 10)); //傳右值
mylist.emplace_back("nxbw", 10); //傳參數(shù)包
return 0;
}由于我們在string的構(gòu)造函數(shù)、拷貝構(gòu)造函數(shù)和移動構(gòu)造函數(shù)當(dāng)中均打印了一條提示語句,因此我們可以通過控制臺輸出來判斷這些函數(shù)是否被調(diào)用。
下面我們用一個容器來存儲模擬實現(xiàn)的string,并以不同的傳參形式調(diào)用emplace系列函數(shù)。比如:

說明一下:
模擬實現(xiàn)string的拷貝構(gòu)造函數(shù)時復(fù)用了構(gòu)造函數(shù),因此在調(diào)用string拷貝構(gòu)造的后面會緊跟著調(diào)用一次構(gòu)造函數(shù)。
為了更好的體現(xiàn)出參數(shù)包的概念,因此這里list容器中存儲的元素類型是pair,我們是通過觀察string對象的處理過程來判斷pair的處理過程的。
這里也可以以不同的傳參方式調(diào)用push_back函數(shù),順便驗證一下容器原有的插入函數(shù)的執(zhí)行邏輯。比如:
int main()
{
list<pair<nxbw::string, int>> mylist;
pair<nxbw::string, int> kv("nxbw", 10);
mylist.push_back(kv); //傳左值
mylist.push_back(pair<nxbw::string, int>("nxbw", 10)); //傳右值
mylist.push_back({ "nxbw", 10 }); //使用列表初始化
return 0;
}
模擬實現(xiàn):emplace接口
namespace nxbw
{
// 模擬實現(xiàn)list在之前的章節(jié)有提過,這里只是將原來的代碼多增加一些接口的片段代碼
// 這是list需要用到的節(jié)點類
template<class T>
struct __list_node
{
__list_node(const T& val = T())
:_data(val), _prev(nullptr), _next(nullptr)
{}
// 這里需要在原來的基礎(chǔ)上需要增加一個可變模板參數(shù)模板的構(gòu)造函數(shù),方便下面使用new
template<class ...Args>
__list_node(Args&& ...args)
: _data(std::forward<Args>(args)...), _prev(nullptr), _next(nullptr)
{}
T _data;
__list_node* _prev;
__list_node* _next;
};
template<class T>
struct list
{
template<class ...Args>
iterator emplace(iterator position, Args&&... args)
{
node* cur = position._node;
node* prev = cur->_prev;
// 函數(shù)參數(shù)包的完美轉(zhuǎn)發(fā)
node* newnode = new node(forward<Args>(args)...);
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(cur);
}
template<class ...Args>
void emplace_back(Args&&... args)
{
// 函數(shù)參數(shù)包的完美轉(zhuǎn)發(fā)
emplace(end(), forward<Args>(args)...);
}
// 獲取節(jié)點函數(shù),這里更新成了萬能引用版的
template<class T>
node* get_node(T&& val = T())
{
node* new_node = new node(forward<T>(val)); // 完美轉(zhuǎn)發(fā)
new_node->_prev = new_node;
new_node->_next = new_node;
return new_node;
}
private:
__list_node<T>* _head; // 指向節(jié)點類的指針
};
};emplace系列和push_back以及insert的區(qū)別
效率方面:對于左值引用版本的push_back和insert來說確實有很大的效率提升,對于右值引用版本的push_back和insert來說效率其實差不多,因為移動賦值/拷貝代價足夠小
構(gòu)造復(fù)雜對象:當(dāng)元素的構(gòu)造比叫復(fù)雜時,emplace可以讓代碼更簡潔,直接傳入構(gòu)造參數(shù)即可
到此這篇關(guān)于C++11中可變模板參數(shù)的實現(xiàn)的文章就介紹到這了,更多相關(guān)C++11 可變模板參數(shù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
教你如何使用qt quick-PathView實現(xiàn)好看的home界面
pathView的使用類似與ListView,都需要模型(model)和代理(delegate),只不過pathView多了一個路徑(path)屬性,顧名思義路徑就是item滑動的路徑,下面給大家分享qt quick-PathView實現(xiàn)好看的home界面,一起看看吧2021-06-06
淺析C/C++ 中return *this和return this的區(qū)別
return *this返回的是當(dāng)前對象的克隆或者本身,return this返回當(dāng)前對象的地址,下面通過本文給大家介紹C/C++ 中return *this和return this的區(qū)別,感興趣的朋友一起看看吧2019-10-10
C語言數(shù)據(jù)結(jié)構(gòu)與算法之排序總結(jié)(二)
這篇文章住要介紹的是選擇類排序中的簡單、樹形和堆排序,歸并排序、分配類排序的基數(shù)排序,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2021-12-12

