• C++ 11中的可变模板参数


    概述

    大家在C++中应该见过不少函数,它们既没有限制参数的类型,也没有限制参数的个数,比如vector<T>::emplace()make_unique<T>()。它们都是利用C++11中的可变模板参数来实现的。对于这一新特性,需要掌握以下三点

    • 可变模板参数的语法
    • 参数包的展开
    • 实践

    前言

    在讲可变模板参数之前,需要先讲C语言中的变长参数

    #include<stdarg.h>
    #define END (-1)
    int sum(int num, ...)
    {
        va_list list;
        va_start(list, num);
        int sum = 0;
        for (int cur = num; cur != END; cur = va_arg(list, int))
            sum += cur;
        va_end(list);
        return sum;
    }
    
    
    int main()
    {
        // 输出15
        cout << sum(1, 2, 3, 4, 5, END) << endl;
    }
    

    C语言中的va_arg宏并不能判断出哪个参数参数包的末尾,所以只能通过自己设定结束位,并通过显式判断来截取有效的参数

    而在C++11中,使用变长参数更简单了(好吧其实也不怎么算变长)

    int sum(std::initializer_list<int> initializerList)
    {
        int sum = 0;
        for (const int& i : initializerList)
            sum += i;
        return sum;
    }
    
    int main()
    {
        std::cout << sum({1, 2, 3, 4, 5}) << std::endl;
    }
    

    可变模板参数的语法

    函数

    以C++11的标准来看,声明一个可变参数的模板函数有两种方法

    template<typename... Ts>
    void AnyNumberOfParam(Ts... ars) {}
    
    int main()
    {
        // 可以接受0及以上个参数
        AnyNumberOfParam();
        AnyNumberOfParam(1, 2);
        AnyNumberOfParam("824", std::vector<int>(), 3);
    }
    
    template<typename T, typename... Ts>
    void AnyNumberOfParam(T least, Ts... ars) {}
    
    int main()
    {
        // 至少需要有一个参数
        // AnyNumberOfParam();
        AnyNumberOfParam(1, 2);
        AnyNumberOfParam("824", std::vector<int>(), 3);
    }
    

    template<typename T, typename... Ts>
    class TestTemplateClass {};
    
    int main()
    {
        TestTemplateClass<int, std::string> t;
    }
    
    template<typename... Ts>
    class TestTemplateClass {};
    
    int main()
    {
        // 因为支持0及以上的参数 所以这么写是合法的
        TestTemplateClass t;
    }
    

    函数参数包的展开

    C++11中的展开方式

    递归展开

    假设我们通过设计一个函数,能逐个输出它的参数

    void print()
    {
        std::cout << "empty" << std::endl;
    }
    template<typename T, typename... Ts>
    void print(T first, Ts... args)
    {
        std::cout << first << std::endl;
        print(args...);
    }
    
    int main()
    {
        print(1, 2, "Mike", 3.21);
        std::cout << std::endl;
        print();
    }
    

    以上代码的递归过程为

    • print(1, 2, "Mike", 3.21);
    • print(2, "Mike", 3.21);
    • print("Mike", 3.21);
    • print(3.21);
    • print();

    通过递归方式展开参数包,当所有参数包展开完毕后,自然为空,所以调用到非模板的递归中止函数

    当然还可以使用模板递归中止函数,这种情况就不支持空参数包了

    template<typename T>
    void print(T end)
    {
        std::cout << end << std::endl;
    }
    template<typename T, typename... Ts>
    void print(T first, Ts... args)
    {
        std::cout << first << std::endl;
        print(args...);
    }
    
    int main()
    {
        print(1, 2, "Mike", 3.21);
        // print();
    }
    

    以上代码的递归过程为

    • print(1, 2, "Mike", 3.21);
    • print(2, "Mike", 3.21);
    • print("Mike", 3.21);
    • print(3.21);

    而在C++17中,对递归展开法进行了优化(前提是将if语句声明为常量表达式)

    template<typename T, typename... Ts>
    void print(T first, Ts... args)
    {
        std::cout << first << std::endl;
        if constexpr(sizeof...(args) > 0)
            print(args...);
    }
    

    逗号表达式搭配initializer_list展开

    在C++11中通过递归展开参数包的缺点很明显,需要重载一个递归终止函数,同时还需要判定终止函数是否需要使用到模板;不仅如此,还需要确保带参数包版本的函数至少包含一个类型(T fistst),可以说是很不便了。下面介绍逗号表达式结合initializer_list的展开方法

    template<typename... Ts>
    void print(Ts&&... args)
    {
        auto lambda = [](auto&& data) { std::cout << data << std::endl; };
        std::initializer_list<int> il = { (lambda(std::forward<Ts>(args)), 0)... };
    }
    
    // C++现代教程上的做法
    template<typename... Ts>
    void print(Ts... args)
    {
        std::initializer_list<int>{([&args]() { std::cout << args << std::endl; }(), 0)... };
    }
    
    int main()
    {
        print(1, 2, "Mike", 3.21);
        print();
    }
    

    这种搭配initializer_list的解法我愿称之为黑魔法,在构造初始化列表的同时完成了参数包的展开

    以下示范一个求和模板函数,如果我们直接使用参数包进行操作而不展开它,那么我们将会得到报错

    C++17中的折叠表达式

    一元折叠表达式

    先来看看如何在C++17中利用折叠表达式(...)展开参数包实现一个求平均数的函数

    template<typename... T>
    int avg(T... args) {
        // 右折叠
        return (args + ...) / sizeof...(args);
    }
    int main()
    {
        // 输出26
        cout << avg(1, 2, 5, 'a') << endl;
        // 编译出错
        // cout << avg() << endl;
    }
    

    对于+运算符,符合交换律,所以左折叠和右折叠的结果相同

    对于-运算符,不符合交换律,因此左右折叠的结果不同

    template<typename... Ts>
    decltype(auto) sub_right(Ts... args) { return (args - ...); }
    
    template<typename... Ts>
    decltype(auto) sub_left(Ts... args) { return (... - args); }
    
    int main()
    {
        // (((10 - 5) - 2) - 8)
        std::cout << sub_left(10, 5, 2, 8) << std::endl;
        // (10 - (5 - (2 - 8)))
        std::cout << sub_right(10, 5, 2, 8) << std::endl;
    }
    

    对于一元折叠表达式而言,只有,&&||操作允许空包,其它的如果出现空包则会编译出错

    When a unary fold is used with a pack expansion of length zero, only the following operators are allowed:

    1. Logical AND (&&). The value for the empty pack is true

    2. Logical OR (||). The value for the empty pack is false

    3. The comma operator (,). The value for the empty pack is void()

    // 一元折叠逗号表达式
    template<typename ...Args>
    void printer(Args&&... args)
    {
        (..., (std::cout << args << std::endl));	// 左折叠 且每个参数间隔输出std::endl
        // ((std::cout << args), ...);	// 右折叠
    }
    
    int main()
    {
        // (((1 << 2) << Mike) << 3.21) 左折叠
        // 1 
     2 
     Mike 
     3.21
        printer(1, 2, "Mike", 3.21);
    }
    

    二元折叠表达式

    二元折叠表达式,支持空包操作。二元折叠表达式的省略号(...)永远在中间,特例在左还是在右决定了是左折叠还是右折叠

    对于std::cout的二元表达式而言,只能使用左折叠(因为输出必须以std::cout开头,而这也就代表了它是左折叠)

    template<typename ...Args>
    void printer(Args&&... args)
    {
        // 二元左折叠
        (std::cout << ... << args);
    }
    
    template<typename ... Ts>
    void printer_space(Ts&&... args)
    {
        auto lambda = [] (auto params)
        {
            cout << ends;
            return params;
        };
        (std::cout << ... << lambda(args));
    }
    
    int main()
    {
        // 12Mike3.21
        printer(1, 2, "Mike", 3.21);
        // nothing...
        printer();
        //  1 2 Mike 3.21
        printer_space(1, 2, "Mike", 3.21);
    }
    

    拓展:以下代码是在参数包展开完毕之后再输出std::endl,而不是每拆一次就输出一次

    template<typename ...Args>
    void printer(Args&&... args)
    {
        (std::cout << ... << args) << std::endl;
    }
    

    如何评价

    Fold expressions with arbitrary callable?

    类参数包的展开

    C++11中的函数参数包可以使用递归或逗号表达式来展开,C++17中则可以使用优化的递归或折叠表达式来展开

    C++11中的类参数包的展开需要运用到类模板的特化(因为笔者也搞不清楚以下代码是属于偏特化还是全特化,所以统一写成特化)

    递归展开

    // 一个支持1及多个类型的类
    template<typename T, typename... Ts>
    class TestClass
    {
    public:
        // 匿名枚举 递归展开
        enum { value = TestClass<T>::value + TestClass<Ts...>::value };
    };
    
    // 对1个类型的情况进行特化 递归中止类
    template<typename lastT>
    class TestClass<lastT>
    {
    public:
        enum { value = sizeof(lastT) };
    };
    
    int main()
    {
        // 4 + 8 + 1 = 13
        std::cout << TestClass<int, double, char>::value << std::endl;
        // 在64位环境下大小为32
        std::cout << TestClass<std::vector<int>>::value << std::endl;
        // TestClass<>::value;
    }
    

    此展开方式不支持0参数包,因此可以改写为以下方式

    // 只声明一个支持0及以上个类型的类
    template<typename... Args>
    class TestClass;
    
    // 对1及以上个类型进行特化
    template<typename First, typename... Rest>
    class TestClass<First, Rest...>
    {
    public:
        // 递归展开
        enum { value = TestClass<First>::value + TestClass<Rest...>::value };
    };
    
    // 对1个类型进行特化 即递归终止类
    template<typename First>
    class TestClass<First>
    {
    public:
        enum { value = sizeof(First) };
    };
    
    // 对0个类型进行特化
    template<>
    class TestClass<>
    {
    public:
        enum { value = 0 };
    };
    

    对上面的代码可能会产生以下几点疑问

    • 为什么只需要声明TestClass主体类而不用实现它,看看以下一个简单的实例你就明白了

      template<typename T>
      class TestTemplate;
      
      template<>
      class TestTemplate<int>
      {
      public:
          int data;
      };
      
      template<>
      class TestTemplate<std::string>
      {
      public:
          std::string name;
      };
      
      int main()
      {
          // 只能实例化出特化的int和string类型
          TestTemplate<int> it{};
          // TestTemplate<char> ct{};
      }
      
    • 为什么感觉这个特化并不是很特化的样子

      template<typename First, typename... Rest>
      class TestClass<First, Rest...> {
          // codes...
      }
      

      因为这里是根据参数的个数进行特化,而不是根据类型进行特化

    你可能会觉得用匿名enum来扮演一个编译期常量有点捞,那么接下来使用同样具有编译期常量特性的std::integral_constant

    注意是integral不是interger

    template<typename T, typename... Ts>
    class TestTemplate : public std::integral_constant<int, TestTemplate<T>::value + TestTemplate<Ts...>::value>
    {
    };
    
    template<typename T>
    class TestTemplate<T> : public std::integral_constant<int, sizeof(T)>
    {
    };
    
    int main()
    {
        cout << TestTemplate<int, double, char>::value << endl;
        // TestTemplate<>::value;
    }
    

    有人可能会说了,那你加一个无类型的特化不就得了吗,其实不是的。因为我们模板类的主体是template<typename T, typename... Ts>,这也就意味着需要1及以上个的类型,那么此时针对一个不满足主体的特化肯定是不正确的。至于如何实现0个类型,上文中给出了匿名enum版本的实现,这里不再赘述

    // 错误的特化方式 编译将出错
    template<>
    class TestTemplate<> : public std::integral_constant<int, 0>
    {
    };
    

    继承展开

    C++11中的std::tuple就是使用继承展开参数包的

    std::tuple可以看作是std::pair的升级版,它支持0-多个类型参数

    std::tuple<int, double, std::string> myTuple{1, 3.14, "pi"};
    auto& [intX, doubleY, strZ] = myTuple;
    intX = 200;
    // 200
    std::cout << std::get<0>(myTuple) << std::endl;
    

    手撕std::tuple的代码将放在后文的实践部分

    实践

    手撕std::tuple

    本文实现的MyTuple的功能十分有限,在实际应用中可能会出现各种不必要的拷贝,以及编译无法通过等问题。以下代码只当作核心功能的剖析,应当作练习看待

    tuple

    // 声明一个支持0及以上个参数包的模板类
    template<typename... Ts>
    class MyTuple;
    
    // 空MyTuple 偷懒不做实现
    template<>
    class MyTuple<> {};
    
    // MyTuple主要实现 继承展开参数包
    template<typename T, typename... Ts>
    class MyTuple<T, Ts...> : public MyTuple<Ts...>
    {
    private:
        T data;
        using TopTuple = MyTuple<Ts...>;
    public:
        MyTuple() = default;
    
        // 通用引用有参构造函数
        template<typename ThisType, typename... RestTypes>
        MyTuple(ThisType&& _data, RestTypes&&... _args) : data(std::forward<ThisType>(_data)), TopTuple(std::forward<RestTypes>(_args)...) {}
    
        // 常函数版本get<>
        template<std::size_t index>
        constexpr auto& get() const
        {
            // 静态断言防止越界访问
            static_assert(index <= sizeof...(Ts), "out of range");
            if constexpr (index == 0)
                return data;
            else
                return TopTuple::template get<index - 1>();
        }
        
        template<std::size_t index>
        constexpr auto& get()
        {
            // 调用常函数版本的get<>
            using element_type = my_tuple_element_type<index, MyTuple<T, Ts...>>;
            return const_cast<element_type&>((static_cast<const MyTuple<T, Ts...>&>(*this)).template get<index>());
        }
    };
    
    int main()
    {
        MyTuple<int, std::string, double> t(1, "Jelly", 3.14);
        // 输出Jelly
        std::cout << t.get<1>() << std::endl;
    }
    

    实现了一个非常简单的MyTuple,没有考虑复杂的拷贝,移动或赋值等问题。和库中的版本一样,都使用了继承的方式展开参数包,每一层MyTuple储存一个类型的数据。

    tuple_size

    下面实现一个个数萃取机,能获得MyTuple中存储的类型个数

    template<typename>
    struct my_tuple_size;
    
    // 对MyTuple格式的类型进行特化
    template<template<typename...> typename TupleType, typename... Ts>
    struct my_tuple_size<TupleType<Ts...>> : std::integral_constant<std::size_t, sizeof...(Ts)> {};
    
    template<typename TupleType>
    inline constexpr std::size_t my_tuple_size_value = my_tuple_size<TupleType>::value;
    

    tuple_element

    再来实现一个类型萃取机,能获得第几号元素是什么类型

    template<std::size_t, typename>
    struct my_tuple_element;
    
    // 对MyTuple格式的类型进行特化
    template<std::size_t index, template<typename...> typename TupleType, typename T, typename... Ts>
    struct my_tuple_element<index, TupleType<T, Ts...>> : my_tuple_element<index - 1, TupleType<Ts...>> {};
    
    // 特化出一个继承终止类
    template<template<typename...> typename TupleType, typename T, typename... Ts>
    struct my_tuple_element<0, TupleType<T, Ts...>> {
        using type = T;
    };
    
    template<std::size_t index, typename TupleType>
    using my_tuple_element_type = typename my_tuple_element<index, TupleType>::type;
    

    make_tuple

    再来实现一个my_make_tuple,下面给出一个错误实现

    template<typename... Ts>
    MyTuple<Ts...> my_make_tuple(Ts&&... args) {
        return MyTuple<Ts...>(std::forward<Ts>(args)...);
    }
    

    乍一看好像很对,对参数包进行展开然后完美转发。这么想就忽略了通用引用的特性,对于左值类型,通用引用会推导出T&;对于右值类型会推导出T

    那么我们使用左值类型进行my_make_tuple时,将会推导出错误的类型

    int main()
    {
        int a = 10;
        const int b = 20;
        string name = "Jelly";
        MyTuple<int&, const int&, std::string&, double> t = my_make_tuple(a, b, name, 3.14);
    }
    

    那么关键就是:用什么办法能去除掉类型的constvolatilereference属性等等呢,答案是退化

    std::decay

    C++ 11中的右值引用中,我提到了std::remove_reference_t,用于去除类型的引用属性,但是std::decay要更猛一些

    • 如果是引用类型,会将其消除
    • 如果又是数组类型,会退化为指针
    • 如果又是函数,会退化为函数指针
    • 同时会消除对象的cv属性

    std::ref

    但是问题又来了,这样不分青红皂白的退化一个类型,也使得我们没有办法创建一个记录引用类型变量的元组。std::ref就是为此而生的,它会将参数包装成std::reference_wrapper对象

    最终实现

    因此还需要进一步特化,正确的实现为:

    template<typename T>
    struct remove_ref_wrap {
        using type = T;
    };
    template<typename T>
    struct remove_ref_wrap<std::reference_wrapper<T>> {
        using type = T&;
    };
    
    template<typename T>
    using with_ref_decay_t = typename remove_ref_wrap<std::decay_t<T>>::type;
    
    template<typename... Ts>
    constexpr MyTuple<with_ref_decay_t<Ts>...> my_make_tuple(Ts&&... args) {
        return MyTuple<with_ref_decay_t<Ts>...>(std::forward<Ts>(args)...);
    }
    

    先执行一次std::decay_t<T>,然后再萃取出std::reference_wrapper对象,为其施加引用。std::decay不会去掉std::reference_wrapper,因为它是经过包装的对象而不是类型

    int main()
    {
        int a = 10;
        float height = 1.7;
        MyTuple<int&, double, float> t = my_make_tuple(std::ref(a), 3.14, height);
    }
    

    不足之处

    对于有隐式转换的类型,需要显式指明my_make_tuple的类型

    MyTuple<std::string> t = my_make_tuple<std::string>("Jelly");
    

    traverse_tuple

    通过传入函数对象进行对MyTuple的遍历

    // 利用折叠表达式执行回调
    template<typename TupleType, typename FuncType, std::size_t... Index>
    void call_tuple(const TupleType& t, const FuncType& f, std::index_sequence<Index...>) {
        (f(t.template get<Index>()), ...);
    }
    
    template<template<typename...> typename TupleType, typename... Ts, typename FuncType>
    void my_traverse_tuple(const TupleType<Ts...>& t, const FuncType& f) {
        call_tuple(t, f, std::make_index_sequence<my_tuple_size_value<TupleType<Ts...>>>{});
    }
    

    测试代码为

    int main()
    {
        MyTuple<int, std::string> t = my_make_tuple(1, "123");
        my_traverse_tuple(t, [](auto&& data) { std::cout << data << std::endl; });
    }
    

    std::apply

    c++17中引入的对std::tuple的一种遍历方式,传入的回调函数的参数为参数包

    int main()
    {
        std::tuple<int, std::string, float> t = std::make_tuple(1, "Jelly", 3.14);
        // 1Jelly3.14
        std::apply([](auto&&... params) { (std::cout << ... << params); }, t);
    }
    

    完善

    经过不懈的努力,我终于掌握了如何解决上面中提到的不足之处

    template<typename... Ts>
    class MyTuple;
    
    template<>
    class MyTuple<> {};
    
    template<typename T, typename... Ts>
    class MyTuple<T, Ts...> : public MyTuple<Ts...>
    {
    private:
        T data;
        using TopTuple = MyTuple<Ts...>;
    
        template<typename TupleType, std::size_t... Indices>
        MyTuple(TupleType&& _copy, std::index_sequence<Indices...>) : MyTuple(_copy.template get<Indices>()...) {}
    
    public:
        MyTuple() = default;
    
        template<typename ThisType, typename... RestTypes>
        MyTuple(ThisType&& _data, RestTypes&&... _args) : data(std::forward<ThisType>(_data)), TopTuple(std::forward<RestTypes>(_args)...) {}
    
        template<template<typename...> typename TupleType, typename... RestTypes>
        MyTuple(TupleType<RestTypes...>&& _copy) : MyTuple(std::forward<TupleType<RestTypes...>>(_copy), std::make_index_sequence<my_tuple_size_value<TupleType<RestTypes...>>>{}) {}
    
        // const MyTuple调用
        template<std::size_t index>
        constexpr auto& get() const
        {
            static_assert(index <= sizeof...(Ts), "out of range");
            if constexpr (index == 0)
                return data;
            else
                return TopTuple::template get<index - 1>();
        }
    
        // non-const MyTuple调用
        template<std::size_t index>
        constexpr auto& get()
        {
            using element_type = my_tuple_element_type<index, MyTuple<T, Ts...>>;
            return const_cast<element_type&>((static_cast<const MyTuple<T, Ts...>&>(*this)).template get<index>());
        }
    };
    

    太棒了,解决了一个问题之后又出现一个问题

    int main()
    {
        MyTuple<int, std::string, float> t1 = my_make_tuple(12, "24142", 3.14);
        cout << t1.template get<0>() << endl;
        cout << t1.template get<1>() << endl;
        cout << t1.template get<2>() << endl;
    
        // 一个我暂时还解决不掉的问题
        // auto temp = my_make_tuple(12, "24142", 3.14);
        // MyTuple<int, std::string, float> t2 = temp;
    }
    

    实现一个泛型delegate

    众所周知,在C#中有一个关键字是delegate

    delegate int AddNumDelegate(int x, int y);
    
    int Sum(int x, int y) { return x + y; }
    
    AddNumDelegate addNum = Sum;
    int z = addNum(10, 30);
    

    如果你看不懂的话,翻译到C++中大概是这么个玩意(图一乐就行了)

    using AddNumDelegate = std::function<int(int, int)>;
    int Sum(int x, int y) { return x + y; }
    
    int main()
    {
        AddNumDelegate addSum = Sum;
        std::cout << addSum(10, 20) << std::endl;
    }
    

    上述例子中都显式指定了函数的参数类型,那我们在C++中实现一个泛型的delegate。因为C#中的函数都是成员函数,所以只实现类成员函数的版本

    template<typename T, typename ReturnType, typename... Params>
    class MyDelegate
    {
    private:
        using MemberFuncType = ReturnType (T::*)(Params...);
        T* pClass;
        MemberFuncType pMemberFunc;
    public:
        MyDelegate(T* _t, MemberFuncType _f) : pClass(_t), pMemberFunc(_f) {}
        
        template<typename... Ts>
        ReturnType operator()(Ts&&... _args) {
            return (pClass->*pMemberFunc)(std::forward<Ts>(_args)...);
        }
    };
    
    struct TestStruct
    {
        template<typename T>
        int test_func(T&& x) { return std::forward<T>(x); }
    };
    
    int main()
    {
        TestStruct t;
        MyDelegate d(&t, &TestStruct::test_func<int>);
        // true
        std::cout << std::boolalpha << is_rvalue_reference_v<decltype(d(10))> << std::endl;
    }
    

    模板题外话

    template<auto>

    这是C++17对非类型模板参数的自动类型推导

    // C++17之前
    template<std::size_t num>
    void print() { std::cout << num << std::endl; }
    // C++17之后
    template<auto num>
    void print() { std::cout << num << std::endl; }
    

    而C++20又放宽了非类型模板参数的限制,C++20之前的非类型模板参数不能为浮点数,只能为int, unsigned int, long long ,char以及指针类型等等,而C++20则支持doublefloat

    // C++20
    template<double num>
    void print() { std::cout << num << std::endl; }
    

    总结

  • 相关阅读:
    python中zip函数
    python中创建列表、元组、字符串、字典
    python中enumerate函数
    python中字符串的拼接
    python中格式化浮点数及科学计数法
    python中tuple函数
    python中可迭代对象的排序
    python中变量类型转换
    python中可迭代对象反转
    python中list函数
  • 原文地址:https://www.cnblogs.com/tuapu/p/15291537.html
Copyright © 2020-2023  润新知