美文网首页C++
手把手教你用c++实现python *args的功能

手把手教你用c++实现python *args的功能

作者: WOWSCpp | 来源:发表于2017-08-17 22:53 被阅读0次

手把手教你在c++中实现python *args的功能

本文大概先通过铺垫3-4节的预备知识便于读者的理解,如有不适,敬请谅解。

一、python *args

相信各位dalao对python里的*args的功能已经是熟悉得不能再熟悉了.

*args使得python对以variable iretable 形式传入的参数的处理变得非常方便。有一个简单的示例:

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
'''

可以看出来,当一个函数的形参是一个variable list,而想传入的参数是一个tuple的时候,使用*args就可以将tuple中的元素映射到形参上。

python中*args的源码具体如何不得而知,因此我利用c++中的std::tuple(since c++11)和varadic template(since c++11)
实现了一个类似功能的template function,并顺带配套了针对tuple的for_each函数和print函数来配合这个template function进行对tuple的遍历和打印,可能有同学好奇为什么不直接用
std::for_each,后面会一并带上源码进行解释。

二、C++ Template MetaProgramming

不要紧张,这里并不会涉及太深的模板元编程(TMP)的知识,但是需要用到其中一个非常核心的思想:递归(recursion)。

了解C++ TMP的同学应该都知道,TMP中最有名的两个例子,一个是求斐波拉契数列,另一个就是求阶乘值。

这里用后者来展现通过递归来实现循环的思想

#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++的模板参数必须是固定长度的,例如:

template <class T> 
void foo(T t)
{}

复杂一点,也可是

template <template <class, class> class C, class K, class v> 
void bar()
{}

上面两种常见的模板生命中,无论template嵌套多少层,每一个模板形参的参数长度只能为1。

但是自从C++11开始,允许每一个模板形参包接受0个或者多个模板实参,模板实参将以逗号','为分隔进行解析。因此上面的两种模板函数的声明可以是这样了:

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这一特性而产生的容器。容器内的元素个数是可变长度的。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

不知道大家发现没有,创建一个tuple非常方便(相对于c++而言),但是貌似tuple没法通过for循环来遍历?

事实确实就是:std::tuple获取元素的方法get<>()函数,不支持变量来遍历,get的模板实参必须是const的!!!

因此要实现对tuple的循环遍历,必须自己实现一个for_each函数(后文有详细讲解)

希望了解更多关于std::tuple知识的同学请点击这里

四、std::forward函数 和 std::move函数简介

提到这两个自从C++11加入的函数,一个不得不提的语义就是rvalue reference(右值引用),限于篇幅原因,本文不详细介绍关于右值引用的细节,也不详细介绍forward和move函数诞生来龙去脉,的感兴趣的同学可以点击这里

在本文中,我们只需要知道,move函数无条件地将左值参数和右值参数转换成一个无名右值(可能出乎很多人的意料,在C++中,无名右值才是真正的右值,具名右值是左值),而forward函数则根据传入参数进行转发:传入的是左值,则转换为左值;传入的是右值,则转换为右值。
根据这一要求,我实现了一个尽可能看上去显得简单的替代版的_move函数和_forward函数,代码如下。

需要特别说明的是,_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)、模板函数中,函数形参直接使用模板形参进行类型推导:
template <class T> void foo(T&& t); // forward reference
template <class T> void bar(std::vector<T>&& t); // rvalue reference 

这个知识点将在后文中得以体现。

预备知识到此结束,下面将进入本文的正题。

_______________________华丽的分割线_______________________

五、针对tuple的for_each

前文已经提到,受限于std::get函数读取tuple时的模板实参必须是const变量,对于一个tuple而言,如果不实现一个类似于std::for_each功能的函数,就只能采用get<0> get<1> get<2> ...的方法来读取, 在程序崩溃之前,程序员就已经崩溃了。

为此,本文实现了3个重载版本的for_each函数。

第一个是针对tuple为non-const lvalue reference的重载版本。
for_each是一个function template,模板形参有3个,分别是:已迭代tuple中的元素个数N,可调用函数F,tuple本身T。
for_each接受两个参数,第一个参数是tuple本身`,第二个参数是函数(仿函数、lambda表达式)的forward reference
enable_if用于重载决议并确定函数返回值类型,sizeof...()返回tuple参数列表的个数(也就是其中的元素个数)。
N从0开始,类似于标准迭代器中的begin(),当N小于tuple中元素个数时,便在函数体中对已经迭代到的元素执行一次函数f(),并且使N+1,再递归调用for_each函数本身。而当N等于tuple中元素个数时,类似于到达标准迭代器的end(),那么在这个函数里则什么都不做。

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()

七、是时候实现python *args的功能了

主要的递归结构如下:
外层模板中,N为tuple参数的个数,F是接受tuple中元素作为参数的函数(的类型),T是tuple(的类型)。
内层模板中,Args则用于对应函数F的参数列表。
请注意:函数、tuple、参数列表都是forward reference形式,因此使用forward对他们进行perfect forwarding以保证他们的左右值的不变。
在递归调用中,从后往前依次取得tuple的元素并放入到参数列表的头部。
代码如下:

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)的时候,我们需要一个特化版本来结束递归。
模板参数的声明去掉unsigned N,Deployer将class template的第一个参数特化为0,成员函数deploy的声明不变,但是做的事变了:不再递归,而是将已经unpack的参数列表直接作为实参传入到函数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)...);
    }
};

至此,我们已经完成了预期的目标,我们可以这样使用我们的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)看上去实在是太晦涩难懂了吗。因此,我们还需要实现几个wrapper来包装一下我们的Deployer::deploy函数,让他看上去更容易使用一些。

八、wrapper的实现

wrapper需要两个模板形参,分别是:函数F(的类型)和参数列表。
使用decltype(auto),我们可以避免自己推导函数的返回值类型。
当然,wrapper函数做的事情,当然就是调用Deployer::deploy函数啦。
最后,我们需要实现3个版本的重载函数。
第一个针对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函数写好了,让我们验证一组复杂的测试用例吧~
需求:传入一个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)

首先实现一个功能函数 restPairWithFirst。

template <class H, class... Body>
auto restPairWithFirst(H&& h, Body&&... body)
{
    return make_tuple(make_pair(forward<H>(h), forward<Body>(body))...);
}

这个函数做了些什么呢?没看懂?好吧,来个例子就懂了。例子中用到了上文中专门为tuple实现的for_each函数,当然,为了输出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)
可以看出,这个函数做的事情就是,将以逗号分隔的参数列表中的第一个元素与后面的元素依次生成一个pair。

有了这个辅助函数,我们离最终目标又进了一步:

template <class... Args>
auto self_XX(Args&&... args) // 笛卡尔乘积又名叉乘,故而简称XX
{
    return tuple_cat(restPairWithFirst(forward<Args>(args), forward<Args>(args)...)...);
} 

self_XX的实现中,对restPairWithFirst函数的调用非常有趣,去掉forward简化来看,就是:restPairWithFirst(args, args...)...,根据前文我们知道,restPairWithFirst是第一个元素与后面的依次生成一个pair,那么如何生成第一个元素和自己的pair呢?很简单,那就是直接在列表的最前面,再加入一个自己,所以便有了上述形式。

我们仍然用一个例子来看看:

for_each(self_XX(1, 2, 3), [](auto&& e) {cout << forward<decltype(e)>(e) << " "; });

此时的输出就已经是预期结果了:(1,1) (1,2) (1,3) (2,1) (2,2) (2,3) (3,1) (3,2) (3,3)

但是!但是!但是!传入self_XX函数的是一个以,分割的序列,并不是tuple啊!那怎么办呢?用我们刚刚实现的wrapper函数呀!
PS.由于模板函数在主函数的特化较为麻烦,需要手动进行特化,因此在最终版的代码中,将上述的self_XX函数改为了仿函数。

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)

终于达到预期啦!

相关文章

网友评论

    本文标题:手把手教你用c++实现python *args的功能

    本文链接:https://www.haomeiwen.com/subject/ffqorxtx.html