手把手教你在c++中實現(xiàn)python *args的功能
本文大概先通過鋪墊3-4節(jié)的預備知識便于讀者的理解,如有不適,敬請諒解。
一、python *args
相信各位dalao對python里的*args的功能已經(jīng)是熟悉得不能再熟悉了.
*args使得python對以variable iretable 形式傳入的參數(shù)的處理變得非常方便。有一個簡單的示例:
def test_args(first, second, third):
print 'First argument: ', first
print 'Second argument: ', second
print 'Third argument: ', third
args = (1, 2, 3)
test_args(*args)
'''
output:
First argument: 1
Second argument: 2
Third argument: 3
'''
可以看出來,當一個函數(shù)的形參是一個variable list,而想傳入的參數(shù)是一個tuple的時候,使用*args就可以將tuple中的元素映射到形參上。
python中*args的源碼具體如何不得而知,因此我利用c++中的std::tuple(since c++11)和varadic template(since c++11)
實現(xiàn)了一個類似功能的template function,并順帶配套了針對tuple的for_each函數(shù)和print函數(shù)來配合這個template function進行對tuple的遍歷和打印,可能有同學好奇為什么不直接用
std::for_each,后面會一并帶上源碼進行解釋。
二、C++ Template MetaProgramming
不要緊張,這里并不會涉及太深的模板元編程(TMP)的知識,但是需要用到其中一個非常核心的思想:遞歸(recursion)。
了解C++ TMP的同學應該都知道,TMP中最有名的兩個例子,一個是求斐波拉契數(shù)列,另一個就是求階乘值。
這里用后者來展現(xiàn)通過遞歸來實現(xiàn)循環(huán)的思想
#include <iostream>
template <unsigned int N>
struct fac
{
static const unsigned int value = N * fac<N - 1>::value;
};
template <>
struct fac<0>
{
static const unsigned int value = 1;
};
int main()
{
std::cout << fac<3>::value << "\n"; // output: 6
return 0;
}
模板類fac通過遞歸不斷求值,并通過以模板實參值為0的模板特化形式結束遞歸。
fac<3>::value = 3*fac<2>::value = 3*2*fac<1>::value = 3*2*1*fac<0>::value,而fac<0>::value = 1,遞歸在fac<0>處結束。
代碼看上去非常簡單,就不再具體解釋了。希望了解更多關于C++ TMP知識的同學,請點擊這里
三、varaidc template 和 std::tuple 簡介
1、varadic template:
對C++比較了解的同學應該知道,98/03標準里,C++的模板參數(shù)必須是固定長度的,例如:
template <class T>
void foo(T t)
{}
復雜一點,也可是
template <template <class, class> class C, class K, class v>
void bar()
{}
上面兩種常見的模板生命中,無論template嵌套多少層,每一個模板形參的參數(shù)長度只能為1。
但是自從C++11開始,允許每一個模板形參包接受0個或者多個模板實參,模板實參將以逗號','為分隔進行解析。因此上面的兩種模板函數(shù)的聲明可以是這樣了:
template <class... Ts>
void foo(Ts... ts)
{}
template<template<class, class...> class ContainerType, class ValueType, class... Args>
void print(const ContainerType<ValueType, Args...>& c)
{
for (const auto& v : c)
{
cout << v << ' ';
}
}
這段代碼可以對stl中的vector,list進行通用的print,結合下面對pair輸入的重載,也可以對map,unorderd_map進行print
ostream& operator<< (ostream& out, const pair<T, U>& p)
{
out << "(" << p.first << "," << p.second << ")";
return out;
}
vector<int> v{ 1, 2};
list<double> l{ 1.0, 2.0};
unordered_map<std::string, int> um;
um.insert(std::make_pair("one", 1));
um.insert(std::make_pair("two", 2));
print(v); // 1 2
print(l); // 1.0 2.0
print(um); //(one,1) (two,2)
希望了解更多關于varadic template知識的同學請點擊這里
2、std::tuple
std::tuple就是利用varadic template這一特性而產(chǎn)生的容器。容器內的元素個數(shù)是可變長度的。std::tuple看上去和python的tuple非常相似。
他可以這樣定義和使用:
auto t1 = std::make_tuple(1, "2", 3);
auto v0 = std::get<0>(t1); // 1
auto v1 = std::get<1>(t1); // "2"
auto v2 = std::get<2>(t1); // 3
不知道大家發(fā)現(xiàn)沒有,創(chuàng)建一個tuple非常方便(相對于c++而言),但是貌似tuple沒法通過for循環(huán)來遍歷?
事實確實就是:std::tuple獲取元素的方法get<>()函數(shù),不支持變量來遍歷,get的模板實參必須是const的?。?!
因此要實現(xiàn)對tuple的循環(huán)遍歷,必須自己實現(xiàn)一個for_each函數(shù)(后文有詳細講解)
希望了解更多關于std::tuple知識的同學請點擊這里
四、std::forward函數(shù) 和 std::move函數(shù)簡介
提到這兩個自從C++11加入的函數(shù),一個不得不提的語義就是rvalue reference(右值引用),限于篇幅原因,本文不詳細介紹關于右值引用的細節(jié),也不詳細介紹forward和move函數(shù)誕生來龍去脈,的感興趣的同學可以點擊這里
在本文中,我們只需要知道,move函數(shù)無條件地將左值參數(shù)和右值參數(shù)轉換成一個無名右值(可能出乎很多人的意料,在C++中,無名右值才是真正的右值,具名右值是左值),而forward函數(shù)則根據(jù)傳入?yún)?shù)進行轉發(fā):傳入的是左值,則轉換為左值;傳入的是右值,則轉換為右值。
根據(jù)這一要求,我實現(xiàn)了一個盡可能看上去顯得簡單的替代版的_move函數(shù)和_forward函數(shù),代碼如下。
需要特別說明的是,_remove_reference是一個type_traits。
template<class T>
typename _remove_reference<T>::type&& _move(T&& t)
{
using returnType = _remove_reference<T>::type;
return static_cast<returnType&&>(t);
}
template<class T>
T&& _forward(typename _remove_reference<T>::type& t)
{
using returnType = T;
return static_cast<returnType&&>(t);
}
template <class T> struct _remove_reference { using type = T; };
template <class T> struct _remove_reference<T&>{ using type = T; };
template <class T> struct _remove_reference<T&&>{ using type = T; };
提到forward,一個不得不說的語義就是forward reference,又稱universal reference。
一些對rvalue reference有所涉獵但研究不是很深的同學可能會認為, T&&就一定是右值引用,但實際情況卻并非如此。
在以下兩種情況下,T&& 并不是rvalue reverence,而是forward reference:
(1)、存在于auto類型推導表達式復制操作符左邊:
auto&& v1 = v2; // forward reference
(2)、模板函數(shù)中,函數(shù)形參直接使用模板形參進行類型推導:
template <class T> void foo(T&& t); // forward reference
template <class T> void bar(std::vector<T>&& t); // rvalue reference
這個知識點將在后文中得以體現(xiàn)。
預備知識到此結束,下面將進入本文的正題。
_______________________華麗的分割線_______________________
五、針對tuple的for_each
前文已經(jīng)提到,受限于std::get函數(shù)讀取tuple時的模板實參必須是const變量,對于一個tuple而言,如果不實現(xiàn)一個類似于std::for_each功能的函數(shù),就只能采用get<0> get<1> get<2> ...的方法來讀取, 在程序崩潰之前,程序員就已經(jīng)崩潰了。
為此,本文實現(xiàn)了3個重載版本的for_each函數(shù)。
第一個是針對tuple為non-const lvalue reference的重載版本。
for_each是一個function template,模板形參有3個,分別是:已迭代tuple中的元素個數(shù)N,可調用函數(shù)F,tuple本身T。
for_each接受兩個參數(shù),第一個參數(shù)是tuple本身`,第二個參數(shù)是函數(shù)(仿函數(shù)、lambda表達式)的forward reference
enable_if用于重載決議并確定函數(shù)返回值類型,sizeof...()返回tuple參數(shù)列表的個數(shù)(也就是其中的元素個數(shù))。
N從0開始,類似于標準迭代器中的begin(),當N小于tuple中元素個數(shù)時,便在函數(shù)體中對已經(jīng)迭代到的元素執(zhí)行一次函數(shù)f(),并且使N+1,再遞歸調用for_each函數(shù)本身。而當N等于tuple中元素個數(shù)時,類似于到達標準迭代器的end(),那么在這個函數(shù)里則什么都不做。
template <unsigned N = 0, class F, class... T>
typename enable_if<N == sizeof...(T), void>::type for_each(tuple<T...>&, F&& f)
{
cout << "non-const lvalue reference end()" << endl; // 便于理解,實際什么都不用做
}
template <unsigned N = 0, class F, class... T>
typename enable_if<N < sizeof...(T), void>::type for_each(tuple<T...>& t, F&& f)
{
f(get<N>(t));
for_each<N + 1, F, T...>(t, forward<F>(f));
}
第二個是針對tuple為const lvalue reference的重載版本
template <unsigned N = 0, class F, class... T>
typename enable_if<N == sizeof...(T), void>::type for_each(const tuple<T...>&, F&& f)
{
cout << "const lvalue reference end()" << endl; // 便于理解,實際什么都不用做
}
template <unsigned N = 0, class F, class... T>
typename enable_if <N < sizeof...(T), void>::type for_each(const tuple<T...>& t, F&& f)
{
f(get<N>(t));
for_each<N + 1, F, T...>(t, forward<F>(f));
}
第三個是針對tuple為rvalue reference的重載版本
template <unsigned N = 0, class F, class... T>
typename enable_if<N == sizeof...(T), void>::type for_each(tuple<T...>&&, F&&)
{
cout << "rvalue reference end()" << endl; // 便于理解,實際什么都不用做
}
template <unsigned N = 0, class F, class... T>
typename enable_if < N < sizeof...(T), void>::type for_each(tuple<T...>&& t, F&& f)
{
f(get<N>(t));
for_each<N + 1, F, T...>(move(t), forward<F>(f));
}
讓我們來測試一下:
auto t1 = make_tuple(1, 2, 3); // non-const lvalue
const auto t2 = make_tuple(4, 5, 6); // const lvalue
for_each(t1, [](auto&& t) {cout << forward<decltype(t)>(t) << ' '; });
for_each(t2, [](auto&& t) {cout << forward<decltype(t)>(t) << ' '; });
for_each(make_tuple(7, 8 ,9), [](auto&& t) {cout << forward<decltype(t)>(t) << ' '; }); // rvalue
結果如下:
1 2 3 non-const lvalue reference end()
4 5 6 const lvalue reference end()
7 8 9 rvalue reference end()
七、是時候實現(xiàn)python *args的功能了
主要的遞歸結構如下:
外層模板中,N為tuple參數(shù)的個數(shù),F(xiàn)是接受tuple中元素作為參數(shù)的函數(shù)(的類型),T是tuple(的類型)。
內層模板中,Args則用于對應函數(shù)F的參數(shù)列表。
請注意:函數(shù)、tuple、參數(shù)列表都是forward reference形式,因此使用forward對他們進行perfect forwarding以保證他們的左右值的不變。
在遞歸調用中,從后往前依次取得tuple的元素并放入到參數(shù)列表的頭部。
代碼如下:
template <unsigned N, class F, class T>
struct Deployer
{
template <class... Args>
static decltype(auto) deploy(F&& f, T&& t, Args&&... args)
{
return Deployer<N - 1, F, T>::deploy(
forward<F>(f),
forward<T>(t),
get<N - 1>(forward<T>(t)),
forward<Args>(args)...);
}
};
當取到tuple中的第一個元素(也就是N = 0)的時候,我們需要一個特化版本來結束遞歸。
模板參數(shù)的聲明去掉unsigned N,Deployer將class template的第一個參數(shù)特化為0,成員函數(shù)deploy的聲明不變,但是做的事變了:不再遞歸,而是將已經(jīng)unpack的參數(shù)列表直接作為實參傳入到函數(shù)f()中進行調用。
代碼如下:
template <class F, class T>
struct Deployer<0, F, T>
{
template <class... Args>
static decltype(auto) deploy(F&& f, T&& t, Args&&... args)
{
return f(forward<Args>(args)...);
}
};
至此,我們已經(jīng)完成了預期的目標,我們可以這樣使用我們的Deployer:
void foo(int i, const std::string& s)
{
cout << "int para: " << i << "\n"
<< "str para: " << s << "\n";
}
auto t = make_tuple(1, "2");
Deployer<2, decltype(foo), decltype(t)&>::deploy(foo, t);
輸出是:
int para: 1
str para: 2
但是,你難道不覺得 Deployer<2, decltype(foo), decltype(t)&>::deploy(foo, t)看上去實在是太晦澀難懂了嗎。因此,我們還需要實現(xiàn)幾個wrapper來包裝一下我們的Deployer::deploy函數(shù),讓他看上去更容易使用一些。
八、wrapper的實現(xiàn)
wrapper需要兩個模板形參,分別是:函數(shù)F(的類型)和參數(shù)列表。
使用decltype(auto),我們可以避免自己推導函數(shù)的返回值類型。
當然,wrapper函數(shù)做的事情,當然就是調用Deployer::deploy函數(shù)啦。
最后,我們需要實現(xiàn)3個版本的重載函數(shù)。
第一個針對tuple為non-const lvalue reference的情況
template <class F, class... Args>
decltype(auto) wrapper(F&& f, tuple<Args...>& t)
{
return Deployer<sizeof...(Args), F, const tuple<Args...>&>::deploy(forward<F>(f), t);
}
第二個針對tuple為const lvalue reference的情況
template <class F, class... Args>
decltype(auto) wrapper(F&& f, const tuple<Args...>& t)
{
return Deployer<sizeof...(Args), F, tuple<Args...>&>::deploy(forward<F>(f), t);
}
第三個針對tuple為rvalue reference的情況
template <class F, class... Args>
decltype(auto) wrapper(F&& f, tuple<Args...>&& t)
{
return Deployer<sizeof...(Args), F, tuple<Args...>&&>::deploy(forward<F>(f), move(t));
}
wrapper函數(shù)寫好了,讓我們驗證一組復雜的測試用例吧~
需求:傳入一個tuple,打印出它的笛卡爾自乘積,需要對各種長度的tuple自適應。
比如,傳入(1,2), 那么需要輸出(1,1) (1,2) (2,1) (2,2)
傳入(1, 2, 3),則需要輸出(1,1) (1,2) (1,3) (2,1) (2,2) (2,3) (3,1) (3,2) (3,3)
首先實現(xiàn)一個功能函數(shù) restPairWithFirst。
template <class H, class... Body>
auto restPairWithFirst(H&& h, Body&&... body)
{
return make_tuple(make_pair(forward<H>(h), forward<Body>(body))...);
}
這個函數(shù)做了些什么呢?沒看懂?好吧,來個例子就懂了。例子中用到了上文中專門為tuple實現(xiàn)的for_each函數(shù),當然,為了輸出pair,我們還需要對 <<進行一次重載。
template <class T, class U>
ostream& operator<< (ostream& out, const pair<T, U>& p) // 重載 <<
{
out << "(" << p.first << "," << p.second << ")";
return out;
}
for_each(restPairWithFirst(1, 2, 3), [](auto&& e) {cout << forward<decltype(e)>(e) << " "; });
輸出為:
(1,2) (1,3)
可以看出,這個函數(shù)做的事情就是,將以逗號分隔的參數(shù)列表中的第一個元素與后面的元素依次生成一個pair。
有了這個輔助函數(shù),我們離最終目標又進了一步:
template <class... Args>
auto self_XX(Args&&... args) // 笛卡爾乘積又名叉乘,故而簡稱XX
{
return tuple_cat(restPairWithFirst(forward<Args>(args), forward<Args>(args)...)...);
}
self_XX的實現(xiàn)中,對restPairWithFirst函數(shù)的調用非常有趣,去掉forward簡化來看,就是:restPairWithFirst(args, args...)...,根據(jù)前文我們知道,restPairWithFirst是第一個元素與后面的依次生成一個pair,那么如何生成第一個元素和自己的pair呢?很簡單,那就是直接在列表的最前面,再加入一個自己,所以便有了上述形式。
我們仍然用一個例子來看看:
for_each(self_XX(1, 2, 3), [](auto&& e) {cout << forward<decltype(e)>(e) << " "; });
此時的輸出就已經(jīng)是預期結果了:(1,1) (1,2) (1,3) (2,1) (2,2) (2,3) (3,1) (3,2) (3,3)
但是!但是!但是!傳入self_XX函數(shù)的是一個以,分割的序列,并不是tuple?。∧窃趺崔k呢?用我們剛剛實現(xiàn)的wrapper函數(shù)呀!
PS.由于模板函數(shù)在主函數(shù)的特化較為麻煩,需要手動進行特化,因此在最終版的代碼中,將上述的self_XX函數(shù)改為了仿函數(shù)。
template <class H, class... Body>
auto restPairWithFirst(H&& h, Body&&... body)
{
return make_tuple(make_pair(forward<H>(h), forward<Body>(body))...);
}
struct self_XX
{
template <class... Args>
auto operator()(Args&&... args)
{
return tuple_cat(pairWithFirst(forward<Args>(args), forward<Args>(args)...)...);
}
};
auto t1 = make_tuple(1, 2);
auto t2 = make_tuple(3, 4, 5);
for_each(wrapper(self_XX(), t1), [](auto&& e) {cout << forward<decltype(e)>(e) << " "; });
for_each(wrapper(self_XX(), t2), [](auto&& e) {cout << forward<decltype(e)>(e) << " "; });
輸出為:
(1,1) (1,2) (2,1) (2,2)
(3,3) (3,4) (3,5) (4,3) (4,4) (4,5) (5,3) (5,4) (5,5)
終于達到預期啦!