美文网首页C++ Templates
【C++ Templates(4)】可变参数模板

【C++ Templates(4)】可变参数模板

作者: downdemo | 来源:发表于2018-04-10 10:31 被阅读29次

可变参数模板示例

#include <iostream>

void print () {} // 没有参数时将调用此函数

template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
    std::cout << firstArg << '\n';  // 打印第一个实参,无参数时将调用此函数
    print(args...); // 调用print()打印其余实参
}

int main()
{
    std::string s("world");
    print(7.5, "hello", s);
}

// output
7.5
hello
world

重载可变参数和非可变参数模板

  • 上例也可以如下实现,如果两个函数模板只有尾置参数包不同,会优先匹配没有尾置参数包的版本
#include <iostream>

template<typename T>
void print (T arg)
{
    std::cout << arg << '\n';
}

template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
    print(firstArg);
    print(args...);
}

sizeof...运算符

  • C++11为可变参数模板引入了sizeof...计算参数包的元素数
template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
    std::cout << sizeof...(Types) << '\n'; // print number of remaining types
    std::cout << sizeof...(args) << '\n'; // print number of remaining args
}
  • 这可能会让我们想到用于跳过递归结尾,以防缺少实参
template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
    std::cout << firstArg << '\n';
    if (sizeof...(args) > 0) { // sizeof...(args)==0时会出错
        print(args...); // 因为print(args...)仍将被初始化,而此时没有实参
    }
}
  • 但这是不行的,因为函数模板中所有的if语句分支都会被实例化,当对最后一个实参调用print()时,打印了实参后,sizeof...(args)为0,但没有实参时print(args...)仍然会初始化,结果就会出错。C++17中引入了编译期if来解决这个问题
template<typename T, typename...Types>
void print(const T& firstArg, const Types&...args)
{
    std::cout << firstArg << '\n';
    if constexpr (sizeof...(args) > 0) {
        print(args...); // code only available if sizeof...(args)>0 (since C++17)
    }
}

折叠表达式

  • C++17有一个特性,用于计算对所有参数包实参使用二元运算符的结果,如下例函数返回所有实参的和
template<typename... T>
auto foldSum (T... s) {
    return (... + s);   // ((s1 + s2) + s3) ...
}
  • 如果参数包为空,表达式通常是非法的(对空参数包例外的是:&&为true,||为false,逗号运算符为void())
折叠表达式
  • 上例中的折叠表达式可以有如下形式
foldSum(1, 2, 3, 4, 5); // 假如实参是12345
// 右边是内部计算时的展开方式
... + s:((((1 + 2) + 3) + 4) + 5)
s + ... :(1 + (2 + (3 + (4 + 5))))
0 + ... + s:(((((0 + 1) + 2) + 3) + 4) + 5)
s + ... + 0:(1 + (2 + (3 + (4 + (5 + 0)))))
  • 折叠表达式几乎可以使用所有二元运算符,比如用operator->*遍历二叉树路径
struct Node {
    int value;
    Node* left;
    Node* right;
    Node(int i = 0) : value(i), left(nullptr), right(nullptr) {}
};

auto left = &Node::left;
auto right = &Node::right;

template<typename T, typename... TP>
Node* traverse (T np, TP... paths) {
    return (np ->* ... ->* paths); // np ->* paths1 ->* paths2 ...
}

int main()
{
    Node* root = new Node{ 0 };
    root->left = new Node{ 1 };
    root->left->right = new Node{ 2 };
    root->left->right->left = new Node{ 3 };
    Node* node1 = traverse(root, left);
    std::cout << node1->value; // 1
    Node* node2 = traverse(root, left, right);
    std::cout << node2->value; // 2
    Node* node3 = traverse(node2, left);
    std::cout << node3->value; // 3
}
  • 使用折叠表达式简化打印所有参数的可变参数模板
template<typename... Args>
void print (const Args&... args)
{
    (std::cout << ... << args) << '\n';
}
  • 如果想用空格分隔参数包元素,为此需要添加一个额外的类模板,并在其中重载operator<<运算符
template<typename T>
class AddSpace
{
private:
    const T& ref; // 构造函数中的实参的引用
public:
    AddSpace(const T& r): ref(r) {}
    friend std::ostream& operator<< (std::ostream& os, AddSpace<T> s) {
        return os << s.ref << ' ';   // 输出传递的实参和一个空格
    }
};

template<typename... Args>
void print (Args... args) {
    (std::cout << ... << AddSpace(args)) << '\n';
}

可变参数模板的应用

  • 可变参数模板的一个典型应用是转发任意数量任意类型的实参,比如通过一个智能指针传递实参给一个新的堆对象的构造
// create shared pointer to complex<float> initialized by 4.2 and 7.7:
auto sp = std::make_shared<std::complex<float>>(4.2, 7.7);
  • 比如传递实参给一个线程
std::thread t (foo, 42, "hello"); // call foo(42,"hello") in a separate thread
std::vector<Customer> v;
v.emplace("Tim", "Jovi", 1962); // insert a Customer initialized by three arguments
  • 通常这类实参会使用移动语义进行完美转发,上述例子在标准库中对应的声明如下
namespace std {
    template<typename T, typename... Args> shared_ptr<T>
    make_shared(Args&&... args);
 
    class thread {
    public:
        template<typename F, typename... Args>
        explicit thread(F&& f, Args&&... args);    
        ...
    };
 
    template<typename T, typename Allocator = allocator<T>>
    class vector {
    public:
        template<typename... Args> reference emplace_back(Args&&... args);
        ...
    };
}
  • 除了上述例子,参数包还能用于其他地方,如表达式,类模板,using声明,deduction guide

可变参数表达式(Variadic Expression)

  • 可以对参数包中的参数进行运算,比如让每个元素翻倍后传递给再打印
#include <iostream>

template<typename... Args>
void print(const Args&... args)
{
    (std::cout << ... << args) << '\n';
}

template<typename... T>
void printDoubled (const T&... args)
{
    print (args + args...);
}

int main()
{
    printDoubled(7.5, std::string("hello"), std::complex<float>(4,2));
    // 等价于
    print(7.5 + 7.5,
        std::string("hello") + std::string("hello"),
        std::complex<float>(4,2) + std::complex<float>(4,2));
}
  • 如果指向给每个元素加1,注意省略号不能直接接在数字字面值后
template<typename... T>
void addOne (const T&... args)
{
    print (args + 1...);    // ERROR: 1... is a literal with too many decimal points
    print (args + 1 ...);   // OK
    print ((args + 1)...);  // OK
}
  • 编译期表达式能以同样的方式包含模板参数包
template<typename T1, typename... TN>
constexpr bool isHomogeneous (T1, TN...)
{ // 判断是否所有实参类型相同
    return (std::is_same<T1, TN>::value && ...); // since C++17
}

isHomogeneous(43, -1, "hello"); // 结果为false
// 扩展为std::is_same<int, int>::value && std::is_same<int, const char*>::value
isHomogeneous("hello", "", "world", "!") // 结果为true:所有实参都为const char*

可变参数索引(Variadic Index)

  • 下面函数使用一个可变索引列表访问传递的第一个实参对应的元素
#include <iostream>

template<typename... Args>
void print(const Args&... args)
{
    (std::cout << ... << args) << '\n';
}

template<typename C, typename... Idx>
void printElems (C const& coll, Idx... idx)
{ 
    print (coll[idx]...);
}

int main()
{
    std::vector<std::string> coll{ "good", "times", "say", "bye" };
    printElems(coll, 2, 0, 3); // saygoodbye
    // 等价于
    print (coll[2], coll[0], coll[3]);
}
  • 非类型模板参数也可以声明为参数包
template<std::size_t... Idx, typename C>
void printIdx (C const& coll)
{
    print(coll[Idx]...);
}

std::vector<std::string> coll{ "good", "times", "say", "bye" };
printIdx<2, 0, 3>(coll);

可变参数类模板(Variadic Class Template)

  • 可变参数类模板的一个重要例子是,用任意数量的模板参数指定对应成员类型
template<typename... Elements>
class Tuple;
 
Tuple<int, std::string, char> t; // t can hold integer, string, and character
  • 另一个例子是指定对象可能的类型
template<typename... Types>
class Variant;

Variant<int, std::string, char> v; // v can hold integer, string, or character
  • 也能定义一个类作为表示一个索引列表的类型
template<std::size_t...>
struct Indices
{};

template<typename T, std::size_t... Idx>
void printByIdx(T t, Indices<Idx...>)
{
    print(std::get<Idx>(t)...);
}

int main()
{
    std::array<std::string, 5> arr{ "Hello", "my", "new", "!", "World" };
    printByIdx(arr, Indices<0, 4, 3>()); // HelloWorld!

    auto t = std::make_tuple(12, "monkeys", 2.0); // 12monkeys2
    printByIdx(t, Indices<0, 1, 2>());
}

可变参数推断指南(Variadic Deduction Guide)

  • C++17的标准库中对std::array定义如下deduction guide
namespace std {
    template<typename T, typename... U> array(T, U...)
        -> array<enable_if_t<(is_same_v<T, U> && ...), T>,
            (1 + sizeof...(U))>;
}

std::array a{42,45,77};
// 等价于
std::array<int, 3> a{42,45,77};

可变参数基类(Variadic Base Class)与using

#include <string>
#include <unordered_set>

class Customer
{
private:
    std::string name;
public:
    Customer(const std::string& n) : name(n) {}
    std::string getName() const { return name; }
};

struct CustomerEq {
    bool operator() (const Customer& c1, const Customer& c2) const {
        return c1.getName() == c2.getName();
    }
};

struct CustomerHash {
    std::size_t operator() (const Customer& c) const {
        return std::hash<std::string>{}(c.getName());
    }
};

// 定义一个组合所有基类的operator()的派生类
template<typename... Bases>
struct Overloader : Bases...
{
    using Bases::operator()...;  // OK since C++17
}; 

int main()
{
    // 将Customer的hasher和equality组合到一个类型中
    using CustomerOP = Overloader<CustomerHash, CustomerEq>;

    /* unordered_set的声明
    template<
    class Key,
        class Hash = std::hash<Key>,
        class KeyEqual = std::equal_to<Key>,
        class Allocator = std::allocator<Key>
    > class unordered_set;
    */

    std::unordered_set<Customer, CustomerHash, CustomerEq> coll1;
    std::unordered_set<Customer, CustomerOP, CustomerOP> coll2;
}

相关文章

网友评论

    本文标题:【C++ Templates(4)】可变参数模板

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