c++11-17 模板核心知識(一)—— 函數模板


1.1 定義函數模板

template<typename T>
T max(T a,T b) {
  return b < a ? a : b;
}

1.2 使用函數模板

  std::cout << max(7,42) << std::endl;

  std::cout << max(1.1,2.2) << std::endl;
  
  std::cout << max("math","mathematics") << std::endl;

模板不是被編譯成可以處理任何類型的單個函數。相反,編譯器會針對每一個使用該模板的類型生成對應的函數。例如,max(7,42)的調用在語義上相當於調用了:

int max(int a,int b) {
  return b < a ? a : b;
}

double、string同理。

將模板參數替換成具體參數類型的過程叫做instantiation,這個過程會產生一個instance of template

image

1.3 兩階段翻譯 Two-Phase Translation

如果某一特定參數類型不支持模板內的操作,那么編譯階段會報錯,例如:

std::complex<float> c1,c2;        //不支持 max中的 < 操作,編譯階段會報錯
...
max(c1,c2);

模板會分成兩個階段進行”編譯“:

  1. 在不進行模板instantiationdefinition time階段,此時會忽略模板參數,檢查如下方面:
    • 語法錯誤,包括缺失分號。
    • 使用未定義參數。
    • 如果static assertion不依賴模板參數,會檢查是否通過static assertion.
  2. instantiation階段,會再次檢查模板里所有代碼的正確性,尤其是那些依賴模板參數的部分。

例如:

template<typename T>
void foo(T t) {
  undeclared();         // first-phase compile-time error if undeclared() unknown

  undeclared(t);       // second-phase compile-time error if undeclared(T) unknown

  static_assert(sizeof(int) > 10,"int too small");      // first-phase compile-time error

  static_assert(sizeof(T) > 10, "T too small");        // second-phase compile-time error

}

image

1.3.1 模板的編譯和鏈接問題

大多數人會按照如下方式組織非模板代碼:

  • 將類或者其他類型聲明放在頭文件(.hpp、.H、.h、.hh、.hxx)中。
  • 將函數定義等放到一個單獨的編譯單元文件中(.cpp、.C、.c、.cc、.cxx)。

但是這種組織方式在包含模板的代碼中卻行不通,例如:
頭文件:

// myfirst.hpp
#ifndef MYFIRST_HPP
#define MYFIRST_HPP
// declaration of template
template<typename T>
void printTypeof (T const&);
#endif // MYFIRST_HPP

定義函數模板的文件:

// myfirst.cpp
#include <iostream>
#include <typeinfo>
#include "myfirst.hpp"
// implementation/definition of template
template<typename T>
void printTypeof (T const& x) {
  std::cout << typeid(x).name() << '\n';
}

在另一個文件中使用該模板:

// myfirstmain.cpp
#include "myfirst.hpp"
// use of the template
int main() {
  double ice = 3.0;
  printTypeof(ice); // call function template for type double
}

在c/c++中,當編譯階段發現一個符號(printTypeof)沒有定義只有聲明時,編譯器會假設它的定義在其他文件中,所以編譯器會留一個”坑“給鏈接器linker,讓它去填充真正的符號地址。

但是上面說過,模板是比較特殊的,需要在編譯階段進行instantiation,即需要進行模板參數類型推斷,實例化模板,當然也就需要知道函數的定義。但是由於上面兩個cpp文件都是單獨的編譯單元文件,所以當編譯器編譯myfirstmain.cpp時,它沒有找到模板的定義,自然也就沒有instantiation

解決辦法就是我們把模板的聲明和定義都放在一個頭文件。大家可以看一下自己環境下的vector等STL源文件,就是把類的聲明和定義都放在了一個文件中。

1.4 多模板參數

template<typename T1, typename T2>
T1 max (T1 a, T2 b) {
    return b < a ? a : b;
}
...
auto m = max(4, 7.2);       // 注意:返回類型是第一個模板參數T1 的類型

但是問題正如注釋中說的,max的返回值類型總是T1。如果我們調用max(42, 66.66),返回值則是66。

一般有三個方法解決這個問題:

  • 引入額外模板參數作為返回值類型
  • 讓編譯器自己找出返回值類型
  • 將返回值聲明為兩個模板參數的公共類型,比如int和float,公共類型就是float

1.4.1 引入額外模板參數作為返回值類型

在函數模板的參數類型推導過程中,一般我們不用顯式指定模板參數類型。但是當模板參數不能根據傳遞的參數推導出來時,我們就需要顯式的指定模板參數類型。

template<typename T1, typename T2, typename RT>
RT max(T1 a, T2 b);

RT是不能根據函數的參數列表推導出來的,所以我們需要顯式的指定:

max<int, double, double>(4, 7.2);

或者我們改變模板參數列表順序,這種情況只需顯式的指定一個參數類型即可:

template<typename RT typename T1, typename T2>      //RT變為第一個模板參數
RT max(T1 a, T2 b);   
...
max<double>(4, 7.2);

1.4.2 讓編譯器自己找出返回值類型

在C++11中,我們可以利用auto和trailing return type來讓編譯器找出返回值類型:

template <typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype(b < a ? a : b) {
  return b < a ? a : b;
}

decltype后面的文章會講到,這里只需知道它可以獲取到表達式的類型。

我們可以寫的更簡單點:

template <typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype(true ? a : b) {      // true ? a : b
  return b < a ? a : b;
}

關於?:返回值規則可以參考這個:Conditional Operator: ? :

看到true ? a : b不要奇怪為什么是true,這里的重點不是計算返回值,而是得到返回值類型。

在C++14中,我們可以省略trailing return type:

template<typename T1, typename T2>
auto max (T1 a, T2 b) {
    return b < a ? a : b;
}

1.4.3 將返回值聲明為兩個模板參數的公共類型

c++11新特性std::common_type可以產生幾個不同類型的共同類型,其實核心意思跟上面說的差不多:

template <typename T1, typename T2>
typename std::common_type<T1, T2>::type max(T1 a, T2 b) {
  return b < a ? a : b;
}

在c++14中,可以更簡單的寫:

template <typename T1, typename T2>
std::common_type_t<T1, T2> max(T1 a, T2 b) {     
  return b < a ? a : b;
}

這里使用_t后綴讓我們不用寫typename::type。類似的還有_v,這個在c++14的type traits里很常見。

image

1.5 默認模板參數

這個很像函數的默認參數,直接看例子:

template <typename T1, typename T2, typename RT = std::common_type_t<T1, T2>>
RT max(T1 a, T2 b) {
  return b < a ? a : b;
}

auto a = max(4, 7.2);
auto b = max<double,int,long double>(7.2, 4);

正如第二個用法,如果我們想顯示的指明RT的類型,必須顯示的指出全部三個參數類型。但是與函數默認參數不同的是,我們可以將默認參數放到第一個位置:

template <typename RT = long, typename T1, typename T2> 
RT max(T1 a, T2 b) {
  return b < a ? a : b;
}

int i;
long l;
…
max(i, l);                     // 返回值類型是long (RT 的默認值)
max<int>(4, 42);      //返回int,因為其被顯式指定

1.6 重載函數模板

這個跟普通函數重載也類似:

// maximum of two int values:
int max(int a, int b) { 
  return b < a ? a : b; 
}

// maximum of two values of any type:
template <typename T> 
T max(T a, T b) { 
  return b < a ? a : b; 
}

int main() {
  max(7, 42);         // calls the nontemplate for two ints
  max(7.0, 42.0);     // calls max<double> (by argument deduction)
  max('a', 'b');      // calls max<char> (by argument deduction)
  max<>(7, 42);       // calls max<int> (by argument deduction)
  max<double>(7, 42); // calls max<double> (no argument deduction)
  max('a', 42.7);     // calls the nontemplate for two ints
}

這里需要解釋下最后一個max('a', 42.7)因為在模板參數類型推導過程中不允許類型自動轉換,但是調用普通函數是允許的,所以這個會調用非模板函數。

ps. 由於函數模板重載,所以函數模板並不像類模板一樣可以進行偏特化。

還有兩點關於重載的基本原則需要了解一下:

1.6.1 重載時最好不要隨便改變模板參數個數,最好可以顯示的指定模板參數類型

下面是段有問題的代碼:

// maximum of two values of any type (call-by-reference)
template <typename T> T const &max(T const &a, T const &b) {
  return b < a ? a : b;
}

// maximum of two C-strings (call-by-value)
char const *max(char const *a, char const *b) {
  return std::strcmp(b, a) < 0 ? a : b;
}

// maximum of three values of any type (call-by-reference)
template <typename T> T const &max(T const &a, T const &b, T const &c) {
  return max(max(a, b), c);           // error if max(a,b) uses call-by-value
}

int main() {
  auto m1 = max(7, 42, 68);         // OK

  char const *s1 = "frederic";
  char const *s2 = "anica";
  char const *s3 = "lucas";
  auto m2 = max(s1, s2, s3);         // run-time ERROR
}

問題出現在return max (max(a,b), c);,因為char const *max(char const *a, char const *b)的參數是按值傳遞,max(a,b)會產生一個指向已經銷毀的棧幀地址,這會導致未定義行為。

1.6.2 確保所有被重載的函數模板在使用時已經被聲明定義

// maximum of two values of any type:
template <typename T> 
T max(T a, T b) {
  std::cout << "max<T>()\n";
  return b < a ? a : b;
}

// maximum of three values of any type:
template <typename T> 
T max(T a, T b, T c) {
  return max(max(a, b), c); 
}

// maximum of two int values:
int max(int a, int b) {
  std::cout << "max(int,int) \n";
  return b < a ? a : b;
}

int main() {
  max(47, 11, 33);    // max<T>()
}

這點很好理解。

(完)

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


免責聲明!

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



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