手把手教你用c++實現(xiàn)python *args的功能

手把手教你在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)

終于達到預期啦!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容