Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

c++11-17 模板核心知识(四)—— 可变参数模板 Variadic Template #154

Open
zhangyachen opened this issue Nov 8, 2020 · 0 comments
Labels

Comments

@zhangyachen
Copy link
Owner

zhangyachen commented Nov 8, 2020

模板参数接收任意数量的参数。

定义与使用

定义:

void print() {}

template <typename T, typename... Types> 
void print(T firstArg, Types... args) {
  std::cout << firstArg << '\n'; // print first argument
  print(args...);                // call print() for remaining arguments
}

使用:

std::string s("world");
print (7.5, "hello", s);

C和GO都有类似的概念和定义方式,很好理解。定义void print() {}是为了终止递归。

args被叫做function parameter pack.

sizeof...

返回parameter pack个数:

template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
  std::cout << sizeof...(Types) << '\n';         // print number of remaining types
  ...
}

也许有人会想利用sizeof...来判断:只有当可变参数模板的参数个数大于0时,才调用print,这样可以省略void print() {}

template <typename T, typename... Types> 
void print(T firstArg, Types... args) {
  std::cout << firstArg << '\n';
  if (sizeof...(args) > 0) { // error if sizeof...(args)==0
    print(args...);          // and no print() for no arguments declared
  }
}

但是这样是错误的,因为模板在编译阶段也会将if的所有代码都进行编译,而不会去根据if的条件去进行选择性的编译,选择运行if的哪个分支是在运行期间做的。

Compile-Time If

但是c++17引入了编译期的if(Compile-Time If),所以上面的代码可以这么写:

template <typename T, typename... Types>
void print(T const &firstArg, Types const &... args) {
  std::cout << firstArg << '\n';
  if constexpr (sizeof...(args) > 0) {
    print(args...); // code only available if sizeof...(args)>0 (since C++17)
  }
}

if constexpr是c++17中编译期if的语法。这样就可以进行在编译期决定编译if条件的哪个分支。再举个例子:

template <typename T>
std::string asString(T x)
{
    if constexpr(std::is_same_v<T, std::string>) {
        return x;   //如果T不是string就是无效的语句
    }
    else if constexpr(std::is_arithmetic_v<T>) {
        return std::to_string(x);   //如果x不是数字就是无效的语句
    }
    else {
        return std::string(x);  //如果不能转换为string就是无效的语句。
    }
}

折叠表达式 Fold Expressions

从c++17开始,折叠表达式可以将二元运算符作用于所有parameter pack的参数上:

Fold Expression Evaluation
( ... op pack ) ((( pack1 op pack2 ) op pack3 ) ... op packN )
( pack op ... ) ( pack1 op ( ... ( packN-1 op packN )))
( init op ... op pack ) ((( init op pack1 ) op pack2 ) ... op packN )
( pack op ... op init ) ( pack1 op ( ... ( packN op init )))

比如求parameter pack的和:

template<typename... T>
auto foldSum (T... s) {
  return (... + s);           // ((s1 + s2) + s3) ...
}

再比如上面的print例子可以简写成:

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

如果想要在每个参数中间输出空格,可以配合lambda:

template <typename FirstType, typename... Args>
void print(FirstType first, Args... args) {
  std::cout << first;

  auto printWhiteSpace = [](const auto arg) { std::cout << " " << arg; };

  (..., printWhiteSpace(args));
}

int main() { 
  print("hello","world","zhangyachen"); 
}

其中, (..., printWhiteSpace(args));会被展开为:printWhiteSpace(arg1), printWhiteSpace(arg2), printWhiteSpace(arg3)这样的格式。

其他场景

Variadic Expressions

比如将每个parameter pack的参数double:

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

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 (T const&... args) {
  print (args + 1...);        // ERROR: 1... is a literal with too many decimal points
  print (args + 1 ...);     // OK
  print ((args + 1)...);    // OK
}

还可以用在Compile-time Expression中,比如下面的函数会判断所有的参数类型是否一致:

template<typename T1, typename... TN>
constexpr bool isHomogeneous (T1, TN...) {
  return (std::is_same<T1,TN>::value && ...);        // since C++17
}

isHomogeneous(43, -1, "hello");

上面的调用会展开为:

std::is_same<int,int>::value && std::is_same<int,char const*>::value       // false

Variadic Indices

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

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

最后的调用相当于:

print (coll[2], coll[0], coll[3]);

Variadic Class Templates

比如标准库的Tuple:

template<typename... Elements>
class Tuple;

Tuple<int, std::string, char> t; // t can hold integer, string, and character

Variadic Deduction Guides

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};

关键点:

  • enable_if_t控制是否启用该模板。 这个后面文章会讲到。
  • is_same_v<T, U> && ...判断数组元素类型是否相同,跟上面提到的例子用法一样。

Variadic Base Classes and using

c++17的新特性,中文翻译应该叫:变长的using声明。C++17尝鲜:变长 using 声明这篇文章关于using的来龙去脉讲的很清楚,推荐大家看看。

一个更实际的例子:

class Customer {
private:
  std::string name;

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

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

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

// define class that combines operator() for variadic base classes:
template <typename... Bases> struct Overloader : Bases... {
  using Bases::operator()...; // OK since C++17
};

int main() {
  // combine hasher and equality for customers in one type:
  using CustomerOP = Overloader<CustomerHash, CustomerEq>;
  std::unordered_set<Customer, CustomerHash, CustomerEq> coll1;
  std::unordered_set<Customer, CustomerOP, CustomerOP> coll2;
  ...
}

这里给unordered_set提供自定义的HashKeyEqual

关于可变参数模板的应用场景和各种使用技巧有很多,这里只列了5种大方向的应用场景,但是起码下次遇到看不懂的地方时,知道往哪个大方向去查,不至于一头雾水 :)

(完)

欢迎大家关注我的知乎账号:https://www.zhihu.com/people/zhangyachen

朋友们可以关注下我的公众号,获得最及时的更新:

image

@zhangyachen zhangyachen changed the title c++11-17 模板核心知识(四)—— 可变参数模板 c++11-17 模板核心知识(四)—— 可变参数模板 Variadic Template Nov 8, 2020
@zhangyachen zhangyachen added the cpp label Nov 9, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant