【C++】C++11可變參數模板(函數模板、類模板)

在C++11之前,類模板和函數模板只能含有固定數量的模板參數。C++11增強了模板功能,允許模板定義中包含0到任意個模板參數,這就是可變參數模板。可變參數模板的加入使得C++11的功能變得更加強大,而由此也帶來了許多神奇的用法。

本文實例源碼github地址https://github.com/yngzMiao/yngzmiao-blogs/tree/master/2020Q2/20200401


可變參數模板

可變參數模板和普通模板的語義是一樣的,只是寫法上稍有區別,聲明可變參數模板時需要在typenameclass後面帶上省略號...

template<typename... Types>

其中,...可接納的模板參數個數是0個及以上的任意數量,需要注意包括0個

若不希望產生模板參數個數爲0的變長參數模板,則可以採用以下的定義:

template<typename Head, typename... Tail>

本質上,...可接納的模板參數個數仍然是0個及以上的任意數量,但由於多了一個Head類型,由此該模板可以接納1個及其以上的模板參數


函數模板的使用

在函數模板中,可變參數模板最常見的使用場景是以遞歸的方法取出可用參數

void print() {}

template<typename T, typename... Types>
void print(const T& firstArg, const Types&... args) {
	std::cout << firstArg << " " << sizeof...(args) << std::endl;
	print(args...);
}

通過設置...,可以向print函數傳遞任意個數的參數,並且各個參數的類型也是任意。也就是說,可以允許模板參數接受任意多個不同類型的不同參數。這就是不定參數的模板,格外需要關注的是,...三次出現的位置

如果如下調用print函數:

print(2, "hello", 1);

如此調用會遞歸將3個參數全部打印。細心的話會發現定義了一個空的print函數,這是因爲當使用可變參數的模板,需要定義一個處理最後情況的函數,如果不寫,會編譯錯誤。這種遞歸的方式,是不是覺得很驚豔!

在不定參數的模板函數中,還可以通過如下方式獲得args的參數個數:

sizeof...(args)

假設,在上面代碼的基礎上再加上一個模板函數如下,那麼運行的結果是什麼呢?

#include <iostream>

void print() {}

template<typename T, typename... Types>
void print(const T& firstArg, const Types&... args) {
	std::cout << firstArg << " " << sizeof...(args) << std::endl;
	print(args...);
}

template <typename... Types>
void print(const Types&... args) {
  std::cout << "print(...)" << std::endl;
}

int main(int argc, char *argv[]) {
	print(2, "hello", 1);

	return 0;
}

現在有一個模板函數接納一個參數加上可變參數,還有一個模板函數直接接納可變參數,如果調用print(2, “hello”, 1),會發現這兩個模板函數的參數格式都符合。是否會出現衝突、不衝突的話會執行哪一個呢?

運行代碼後的結果爲:

yngzmiao@yngzmiao-virtual-machine:~/test/$ ./main 
2 2
hello 1
1 0

從結果上可以看出,程序最終選擇了一個參數加上不定參數的模板函數。也就是說,當較泛化和較特化的模板函數同時存在的時候,最終程序會執行較特化的那一個

再比如一個例子,std::max函數只可以返回兩個數的較大者,如果多個數,就可以通過不定參數的模板來實現:

#include <iostream>

template <typename T>
T my_max(T value) {
  return value;
}

template <typename T, typename... Types>
T my_max(T value, Types... args) {
  return std::max(value, my_max(args...));
}

int main(int argc, char *argv[]) {
  std::cout << my_max(1, 5, 8, 4, 6) << std::endl;

	return 0;
}

類模板的使用

除了函數模板的使用外,類模板也可以使用不定參數的模板參數,最典型的就是tuple類了。其大致代碼如下:

#include <iostream>

template<typename... Values> class tuple;
template<> class tuple<> {};

template<typename Head, typename... Tail>
class tuple<Head, Tail...>
  : private tuple<Tail...>
{
  typedef tuple<Tail...> inherited;
  public:
    tuple() {}
    tuple(Head v, Tail... vtail) : m_head(v), inherited(vtail...) {}
    Head& head() {return m_head;}
    inherited& tail() {return *this;}
  protected:
    Head m_head;
};

int main(int argc, char *argv[]) {
	tuple<int, float, std::string> t(1, 2.3, "hello");
	std::cout << t.head() << " " << t.tail().head() << " " << t.tail().tail().head() << std::endl;

	return 0;
}

根據代碼可以知道,tuple類繼承除首之外的其他參數的子tuple類,以此類推,最終繼承空參數的tuple類。繼承關係可以表述爲:

tuple<>
      ↑
tuple<std::string>
  string "hello"
      ↑
tuple<float, std::string>
  float 2.3
      ↑
tuple<int, float, std::string>
  int 1

接下來考慮在內存中的分佈,內存中先存儲父類的變量成員,再保存子類的變量成員,也就是說,對象t按照內存分佈來說;

┌─────────┐<---- 對象指針
|  hello  |
|─────────|
|  2.3    |
|─────────|
|  1      |
└─────────┘

這時候就可以知道下一句代碼的含義了:

inherited& tail() {return *this;}

tail()函數返回的是父類對象,父類對象和子類對象的內存起始地址其實是一樣的,因此返回*this,再強行轉化爲inherited類型。

當然,上面採用的是遞歸繼承的方式,除此之外,還可以採用遞歸複合的方式:

template<typename... Values> class tup;
template<> class tup<> {};

template<typename Head, typename... Tail>
class tup<Head, Tail...>
{
  typedef tup<Tail...> composited;
  public:
    tup() {}
    tup(Head v, Tail... vtail) : m_head(v), m_tail(vtail...) {}
    Head& head() {return m_head;}
    composited& tail() {return m_tail;}
  protected:
    Head m_head;
    composited m_tail;
};

兩種方式在使用的過程中沒有什麼區別,但C++11中採用的是第一種方式(遞歸繼承)。

在上面的例子中,取出tuple中的元素是一個比較複雜的操作,需要不斷地取tail,最終取head才能獲得。標準庫的std::tuple,對此進行簡化,還提供了一些其他的函數來進行對tuple的訪問。例如:

#include <iostream>
#include <tuple>

int main(int argc, char *argv[]) {
  std::tuple<int, float, std::string> t2(1, 2.3, "hello");
  std::get<0>(t2) = 4;                      // 修改tuple內的元素
  std::cout << std::get<0>(t2) << " " << std::get<1>(t2) << " " << std::get<2>(t2) << std::endl;    // 獲取tuple內的元素

  auto t3 = std::make_tuple(2, 3.4, "World");         // make方法生成tuple對象
  
  std::cout << std::tuple_size<decltype(t3)>::value << std::endl;    // 獲取tuple對象元素的個數
  std::tuple_element<1, decltype(t3)>::type f = 1.2;          // 獲取tuple對象某元素的類型

	return 0;
}

相關閱讀

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章