c++11-17 模板核心知識(四)—— 可變參數模板 Variadic Template


模板參數接收任意數量的參數。

定義與使用

定義:

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種大方向的應用場景,但是起碼下次遇到看不懂的地方時,知道往哪個大方向去查,不至於一頭霧水 :)

(完)

朋友們可以關注下我的公眾號,獲得最及時的更新:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM