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 模板核心知识(一)—— 函数模板 #151

Open
zhangyachen opened this issue Nov 3, 2020 · 0 comments
Open

c++11-17 模板核心知识(一)—— 函数模板 #151

zhangyachen opened this issue Nov 3, 2020 · 0 comments
Labels

Comments

@zhangyachen
Copy link
Owner

zhangyachen commented Nov 3, 2020

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>()
}

这点很好理解。

(完)

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

image

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