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



emplace接口全部都是使用的可變參數(shù)模板
注意:兩個(gè)&&是萬(wàn)能引用并不是右值引用
對(duì)比list中的push_back和emplace_back,對(duì)于emplace系列接口而言,它的主要優(yōu)勢(shì)就是直接在容器內(nèi)部構(gòu)造元素可以結(jié)合我上面給的場(chǎng)景進(jìn)行理解,而不是構(gòu)造一個(gè)臨時(shí)對(duì)象在復(fù)制或移動(dòng)到容器中可以有效的避免拷貝和移動(dòng)操作
以emplace和push_back為例:
調(diào)用push_back函數(shù)插入元素時(shí),可以傳入左值對(duì)象或者右值對(duì)象,也可以使用列表初始化
調(diào)用emplace時(shí)可以傳左值對(duì)象或者右值對(duì)象,但是不能使用列表初始化,emplace系列最大的特點(diǎn)就是,插入元素時(shí)可以傳入用于構(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ù),容器會(huì)直接在emplace接口的實(shí)現(xiàn)中構(gòu)造該對(duì)象
emplace系列接口的工作流程
emplace系列接口的工作流程如下:
- 先通過(guò)空間配置器為新結(jié)點(diǎn)獲取一塊內(nèi)存空間,注意這里只會(huì)開(kāi)辟空間,不會(huì)自動(dòng)調(diào)用構(gòu)造函數(shù)對(duì)這塊空間進(jìn)行初始化。
- 然后調(diào)用allocator_traits::construct函數(shù)對(duì)這塊空間進(jìn)行初始化,調(diào)用該函數(shù)時(shí)會(huì)傳入這塊空間的地址和用戶傳入的參數(shù)(需要經(jīng)過(guò)完美轉(zhuǎn)發(fā))。
- 在allocator_traits::construct函數(shù)中會(huì)使用定位new表達(dá)式,顯示調(diào)用構(gòu)造函數(shù)對(duì)這塊空間進(jìn)行初始化,調(diào)用構(gòu)造函數(shù)時(shí)會(huì)傳入用戶傳入的參數(shù)(需要經(jīng)過(guò)完美轉(zhuǎn)發(fā))。
- 將初始化好的新結(jié)點(diǎn)插入到對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)當(dāng)中,比如list容器就是將新結(jié)點(diǎn)插入到底層的雙鏈表中。
emplace系列接口的意義
由于emplace系列接口的可變模板參數(shù)的類型都是萬(wàn)能引用,因此既可以接收左值對(duì)象,也可以接收右值對(duì)象,還可以接收參數(shù)包。
- 如果調(diào)用emplace系列接口時(shí)傳入的是左值對(duì)象,那么首先需要先在此之前調(diào)用構(gòu)造函數(shù)實(shí)例化出一個(gè)左值對(duì)象,最終在使用定位new表達(dá)式調(diào)用構(gòu)造函數(shù)對(duì)空間進(jìn)行初始化時(shí),會(huì)匹配到拷貝構(gòu)造函數(shù)。
- 如果調(diào)用emplace系列接口時(shí)傳入的是右值對(duì)象,那么就需要在此之前調(diào)用構(gòu)造函數(shù)實(shí)例化出一個(gè)右值對(duì)象,最終在使用定位new表達(dá)式調(diào)用構(gòu)造函數(shù)對(duì)空間進(jìn)行初始化時(shí),就會(huì)匹配到移動(dòng)構(gòu)造函數(shù)。
- 如果調(diào)用emplace系列接口時(shí)傳入的是參數(shù)包,那就可以直接調(diào)用函數(shù)進(jìn)行插入,并且最終在使用定位new表達(dá)式調(diào)用構(gòu)造函數(shù)對(duì)空間進(jìn)行初始化時(shí),匹配到的是構(gòu)造函數(shù)。
總結(jié)一下:
- 傳入左值對(duì)象,需要調(diào)用構(gòu)造函數(shù)+拷貝構(gòu)造函數(shù)。
- 傳入右值對(duì)象,需要調(diào)用構(gòu)造函數(shù)+移動(dòng)構(gòu)造函數(shù)。
- 傳入?yún)?shù)包,只需要調(diào)用構(gòu)造函數(shù)。
當(dāng)然,這里的前提是容器中存儲(chǔ)的元素所對(duì)應(yīng)的類,是一個(gè)需要深拷貝的類,并且該類實(shí)現(xiàn)了移動(dòng)構(gòu)造函數(shù)。否則在調(diào)用emplace系列接口時(shí),傳入左值對(duì)象和傳入右值對(duì)象的效果都是一樣的,都需要調(diào)用一次構(gòu)造函數(shù)和一次拷貝構(gòu)造函數(shù)。
實(shí)際emplace系列接口的一部分功能和原有各個(gè)容器插入接口是重疊的,因?yàn)槿萜髟械膒ush_back、push_front和insert函數(shù)也提供了右值引用版本的接口,如果調(diào)用這些接口時(shí)如果傳入的是右值對(duì)象,那么最終也是會(huì)調(diào)用對(duì)應(yīng)的移動(dòng)構(gòu)造函數(shù)進(jìn)行資源的移動(dòng)的。
emplace接口的意義:
emplace系列接口最大的特點(diǎn)就是支持傳入?yún)?shù)包,用這些參數(shù)包直接構(gòu)造出對(duì)象,這樣就能減少一次拷貝,這就是為什么有人說(shuō)emplace系列接口更高效的原因。
但emplace系列接口并不是在所有場(chǎng)景下都比原有的插入接口高效,如果傳入的是左值對(duì)象或右值對(duì)象,那么emplace系列接口的效率其實(shí)和原有的插入接口的效率是一樣的。
emplace系列接口真正高效的情況是傳入?yún)?shù)包的時(shí)候,直接通過(guò)參數(shù)包構(gòu)造出對(duì)象,避免了中途的一次拷貝。
通過(guò)下面的場(chǎng)景我們來(lái)驗(yàn)證一下:
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;
}
// 移動(dòng)構(gòu)造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移動(dòng)語(yǔ)義" << endl;
swap(s);
}
// 移動(dòng)賦值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移動(dòng)語(yǔ)義" << 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)識(shí)的\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;
}由于我們?cè)趕tring的構(gòu)造函數(shù)、拷貝構(gòu)造函數(shù)和移動(dòng)構(gòu)造函數(shù)當(dāng)中均打印了一條提示語(yǔ)句,因此我們可以通過(guò)控制臺(tái)輸出來(lái)判斷這些函數(shù)是否被調(diào)用。
下面我們用一個(gè)容器來(lái)存儲(chǔ)模擬實(shí)現(xiàn)的string,并以不同的傳參形式調(diào)用emplace系列函數(shù)。比如:

說(shuō)明一下:
模擬實(shí)現(xiàn)string的拷貝構(gòu)造函數(shù)時(shí)復(fù)用了構(gòu)造函數(shù),因此在調(diào)用string拷貝構(gòu)造的后面會(huì)緊跟著調(diào)用一次構(gòu)造函數(shù)。
為了更好的體現(xiàn)出參數(shù)包的概念,因此這里list容器中存儲(chǔ)的元素類型是pair,我們是通過(guò)觀察string對(duì)象的處理過(guò)程來(lái)判斷pair的處理過(guò)程的。
這里也可以以不同的傳參方式調(diào)用push_back函數(shù),順便驗(yàn)證一下容器原有的插入函數(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;
}
模擬實(shí)現(xiàn):emplace接口
namespace nxbw
{
// 模擬實(shí)現(xiàn)list在之前的章節(jié)有提過(guò),這里只是將原來(lái)的代碼多增加一些接口的片段代碼
// 這是list需要用到的節(jié)點(diǎn)類
template<class T>
struct __list_node
{
__list_node(const T& val = T())
:_data(val), _prev(nullptr), _next(nullptr)
{}
// 這里需要在原來(lái)的基礎(chǔ)上需要增加一個(gè)可變模板參數(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é)點(diǎn)函數(shù),這里更新成了萬(wàn)能引用版的
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é)點(diǎn)類的指針
};
};emplace系列和push_back以及insert的區(qū)別
效率方面:對(duì)于左值引用版本的push_back和insert來(lái)說(shuō)確實(shí)有很大的效率提升,對(duì)于右值引用版本的push_back和insert來(lái)說(shuō)效率其實(shí)差不多,因?yàn)橐苿?dòng)賦值/拷貝代價(jià)足夠小
構(gòu)造復(fù)雜對(duì)象:當(dāng)元素的構(gòu)造比叫復(fù)雜時(shí),emplace可以讓代碼更簡(jiǎn)潔,直接傳入構(gòu)造參數(shù)即可
到此這篇關(guān)于C++11中可變模板參數(shù)的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)C++11 可變模板參數(shù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
教你如何使用qt quick-PathView實(shí)現(xiàn)好看的home界面
pathView的使用類似與ListView,都需要模型(model)和代理(delegate),只不過(guò)pathView多了一個(gè)路徑(path)屬性,顧名思義路徑就是item滑動(dòng)的路徑,下面給大家分享qt quick-PathView實(shí)現(xiàn)好看的home界面,一起看看吧2021-06-06
淺析C/C++ 中return *this和return this的區(qū)別
return *this返回的是當(dāng)前對(duì)象的克隆或者本身,return this返回當(dāng)前對(duì)象的地址,下面通過(guò)本文給大家介紹C/C++ 中return *this和return this的區(qū)別,感興趣的朋友一起看看吧2019-10-10
VS2022配置編譯使用boost庫(kù)的實(shí)現(xiàn)
本文介紹了如何在VS2022中配置和編譯使用Boost庫(kù)的步驟,包括下載Boost、解壓、配置環(huán)境變量和編譯等過(guò)程,具有一定的參考價(jià)值,感興趣的可以了解一下2024-12-12
C語(yǔ)言數(shù)據(jù)結(jié)構(gòu)與算法之排序總結(jié)(二)
這篇文章住要介紹的是選擇類排序中的簡(jiǎn)單、樹形和堆排序,歸并排序、分配類排序的基數(shù)排序,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2021-12-12

