15 特化与重载

重载模板以适用不同的情况

  • 下面用于交换两个对象的函数模板exchange可以处理简单类型,但如果T是下面的类,就没必要再拷贝一次对象并调用两次赋值运算符,而只需要使用成员模板exchangeWith交换内部的指针
template<typename T>
class Array {
 public:
  Array(const Array<T>&);
  Array<T>& operator=(const Array<T>&);
  void exchangeWith(Array<T>* b)
  {
    T* tmp = data;
    data = b->data;
    b->data = tmp;
  }
  T& operator[](std::size_t k)
  {
    return data[k];
  }
 private:
  T* data;
};

template<typename T> inline
void exchange(T* a, T* b)
{
  T tmp(*a);
  *a = *b;
  *b = tmp;
}
  • 但使用成员模板的用户要记忆并在适当情况下使用这个接口,为了免去这个负担,重载函数模板即可实现一致性
template<typename T> inline
void quick_exchange(T* a, T* b) // (1)
{
  T tmp(*a);
  *a = *b;
  *b = tmp;
}

template<typename T> inline
void quick_exchange(Array<T>* a, Array<T>* b) // (2)
{
  a->exchange_with(b);
}

void demo(Array<int>* p1, Array<int>* p2)
{
  int x = 42, y = -7;
  quick_exchange(&x, &y); // 使用(1)
  quick_exchange(p1, p2); // 使用(2):重载会优先选择更加特殊的模板
}
  • 虽然上述两种交换的算法都能交换指针指向的值,但各自的副作用截然不同
struct S {
  int x;
} s1, s2;

void distinguish(Array<int> a1, Array<int> a2)
{
  a1[0] = 1;
  a2[0] = 2;
  s1.x = 3;
  s2.x = 4;
  int* p = &a1[0]; // p和a1->data指向同一处,此处存储的值为1
  int* q = &s1.x; // q指向s1.x,s1.x的值为3
  quick_exchange(&a1, &a2); // a1->data和a2->data交换了,现在换a2->data和p指向1
  quick_exchange(&s1, &s2); // s1和s2的值交换了,s1.x现在为4,所以*q为4
}
  • 如果不明白参考下面代码
int main()
{
  int* a = new int[3];
  a[0] = 1;
  int* p = &a[0]; // 把a现在的地址赋值给p,之后a指向哪都不会改变p

  int* b = new int[3];
  b[0] = 2;
  int* q = b;
  b = a; // 此时abp都指向一处
  a = q; // a指向了原来b的那处,bp指向一处
  std::cout << a[0] <<  b[0] << *p; // 211
}
// 更简单的例子
int main()
{
  int i = 1, j = 2;
  int* p = &i; // 把i的地址赋值给p
  int* q = p; // 把p的值,即i的地址赋值给q
  p = &j; // p的值改为j的地址,但q的值仍为i的地址
  std::cout << *q; // 1
}
  • 如果不使用成员模板exchangeWith,可以将函数改为递归调用(因此不再使用inline)
template<typename T>
void quick_exchange(T* a, T* b)
{
  T tmp(*a);
  *a = *b;
  *b = tmp;
}

template<typename T>
void quick_exchange(Array<T>* a, Array<T>* b)
{
  T* p = &(*a)[0];
  T* q = &(*b)[0];
  for (std::size_t k = a->size(); k-- != 0; )
  {
    quick_exchange(p++, q++); // 如果T也是Array则递归调用此模板
  }
}

签名(Signature)

  • 只要具有不同的签名,两个函数就能同时存在于同一个程序中,函数签名(简单理解就是函数声明中所包含的信息)定义如下
    • 函数的非受限名称
    • 函数名称所属的作用域
    • 函数的cv限定符
    • 函数的&或&&限定符
    • 函数参数类型
    • 函数模板还包括返回类型、模板参数和模板实参
  • 以下模板具有不同的签名,其实例化体可以在一个程序中同时存在
template<typename T1, typename T2>
void f(T1, T2);

template<typename T1, typename T2>
void f(T2, T1);

template<typename T>
long g(T);

template<typename T>
char g(T);
  • 但上述模板在同一作用域中时的实例化可能导致二义性
#include <iostream>

template<typename T1, typename T2>
void f(T1, T2) {}

template<typename T1, typename T2>
void f(T2, T1) {}

int main()
{
  f<char, char>('a', 'b'); // 二义性错误
}
  • 只有两个模板出现在不同的编译单元,两个实例化体才能在一个程序中同时存在
// Translation unit 1:
#include <iostream>

template<typename T1, typename T2>
void f(T1, T2)
{
  std::cout << 1;
}

void g()
{
  f<char, char>('a', 'b');
}

// Translation unit 2:
#include <iostream>

template<typename T1, typename T2>
void f(T2, T1)
{
  std::cout << 2;
}

extern void g(); // defined in translation unit 1

int main()
{
  f<char, char>('a', 'b'); // 2
  g(); // 1
}

函数模板的偏序(Partial Ordering)规则

  • 多个函数模板匹配实参列表时,C++规定了偏序规则来决定调用哪个模板,之所以叫偏序是因为一些模板可以是一样特殊的,此时则会出现二义性调用
#include <iostream>

template<typename T>
void f(T)
{
  std::cout << 1;
}

template<typename T>
void f(T*)
{
  std::cout << 2;
}

int main()
{
  f<int*>((int*)nullptr); // 1:重载解析
  f<int>((int*)nullptr); // 2:重载解析
  f(0); // 1:0被推断为int,无重载解析,匹配第一个模板
  f(nullptr); // 1
  f((int*)nullptr); // 2:重载解析,第二个更特殊
}
  • 决定哪个模板更特殊的规则为:考虑两个模板T1和T2,不考虑省略号参数和默认实参,用假想的类型X替换T1,看T2是否能推断T1的参数列表(忽略隐式转换),再把T1和T2反过来做一次这个过程,能被推断的那个就是更特殊的。由这个规则推出的一些结论:特化比泛型特殊、T*比T特殊、const T比T特殊、const T*T*特殊
template<typename T>
void f(T*, const T* = nullptr, ...);

template<typename T>
void f(const T*, T*, T* = nullptr);

void example(int* p)
{
  f(p, p); // 错误:二义性调用,两个模板没有偏序关系,一样特殊
}

普通函数会被重载优先考虑

  • 函数模板也可以和非模板的普通函数重载,其他条件相同时优先调用非模板函数
template<typename T>
void f(T)
{
  std::cout << 1;
}

void f(int&)
{
  std::cout << 2;
}

int main()
{
  int x = 0;
  f(x); // 2
}
  • 当有const和引用限定符时重载解析会改变
template<typename T>
void f(T)
{
  std::cout << 1;
}

void f(const int&)
{
  std::cout << 2;
}

int main()
{
  int x = 0;
  f(x); // 1
  const int y = 0;
  f(y); // 2
}
  • 但这对特殊的成员模板会造成意外的行为
class C {
 public:
  C() = default;
  C(const C&) { std::cout << 1; }
  C(C&&) { std::cout << 2; }
  template<typename T>
  C(T&&) { std::cout << 3; }
};

int main()
{
  C x;
  C x2{x}; // 3:对于non-const,成员模板是比拷贝构造函数更好的匹配
  C x3{std::move(x)}; // 2
  const C c;
  C x4{c}; // 1
  C x5{std::move(c)}; // 3:对于const C&&(尽管不常见),成员模板比移动构造函数更好
}

可变参数函数模板的重载

#include <iostream>

template<typename T>
void f(T*)
{
  std::cout << 1;
}

template<typename... Ts>
void f(Ts...)
{
  std::cout << 2;
}

template<typename... Ts>
void f(Ts*...)
{
  std::cout << 3;
}

int main()
{
  f(0, 0.0); // 2
  f((int*)nullptr, (double*)nullptr); // 3
  f((int*)nullptr); // 1
}
  • 包扩展同理
#include <iostream>

template<typename... Ts>
class Tuple {};

template<typename T>
void f(Tuple<T*>)
{
  std::cout << 1;
}

template<typename... Ts>
void f(Tuple<Ts...>)
{
  std::cout << 2;
}

template<typename... Ts>
void f(Tuple<Ts*...>)
{
  std::cout << 3;
}

int main()
{
  f(Tuple<int, double>()); // 2
  f(Tuple<int*, double*>()); // 3
  f(Tuple<int*>()); // 1
}

函数模板全特化

  • 函数模板特化引入了重载和实参推断,如果能推断特化版本,就可以不显式声明模板实参
template<typename T>
int f(T) // (1)
{
  return 1;
}

template<typename T>
int f(T*) // (2)
{
  return 2;
}

template<>
int f(int) // OK:(1)的特化
{
  return 3;
}

template<>
int f(int*) // OK:(2)的特化
{
  return 4;
}
  • 函数模板特化不能有默认实参,但会使用要被特化的模板的默认实参
template<typename T>
int f(T, T x = 42)
{
  return x;
}

template<>
int f(int, int = 35) // 错误
{
  return 0;
}

template<typename T>
int g(T, T x = 42)
{
  return x;
}

template<>
int g(int, int y)
{
  return y/2;
}

int main()
{
  std::cout << g(0) << std::endl; // 21
}
  • 特化声明的不是一个模板,非内联的函数模板特化在同个程序中的定义只能出现一次,通常应该把特化的实现写在源文件中。如果想定义在头文件内,可以把特化声明为内联函数
#ifndef TEMPLATE_G_HPP
#define TEMPLATE_G_HPP

// 模板定义应放在头文件中
template<typename T>
int g(T, T x = 42)
{
  return x;
}

// 特化声明会阻止模板实例化
// 为避免重定义错误不在此定义
template<>
int g(int, int y);

#endif // TEMPLATE_G_HPP

// 实现文件
#include "template_g.hpp"
template<>
int g(int, int y)
{
  return y/2;
}

类模板全特化

  • 特化的实参列表必须对应模板参数,非类型值不能替换类型参数,如果有默认实参可以不指定对应参数
template<typename T>
class Types {
 public:
  using I = int;
};

template<typename T, typename U = typename Types<T>::I>
class X; // (1)

template<>
class X<void> { // (2)使用默认实参:X<void, int>
 public:
  void f();
};

template<> class X<char, char>; // (3)

template<> class X<char, 0>; // 错误:0不能替换U

int main()
{
  X<int>* pi; // OK:使用(1),不需要定义
  X<int> e1; // 错误:使用(1)但没有定义
  X<void>* pv; // OK:使用(2)
  X<void, int> sv; // OK:使用(2)
  X<void, char> e2; // 错误:使用(1)但没有定义
  X<char, char> e3; // 错误:使用(3)但没有定义
}

template<>
class X<char, char> {} // (3)的定义
  • 类特化和泛型模板的唯一区别是,特化不能单独出现,必须有一个模板的前置声明。特化声明不是模板声明,所以类外定义成员时应该使用普通的定义语法(即不指定template<>前缀)
template<typename T>
class X; // 必须有此前置声明才能特化

template<>
class X<char**> {
 public:
  void print() const;
};

// 下面的定义不能使用template<>前缀
void X<char**>::print() const
{
  std::cout << "pointer to pointer to char\n";
}

// 另一个例子
template<typename T>
class A {
 public:
  template<typename U>
  class B {};
};

template<>
class A<void> {
  // 下面的嵌套类和上面定义的泛型模板之间并不存在联系
  template<typename U>
  class B {
   private:
    static int i;
  };
};

// 下面的定义不能使用template<>前缀
template<typename U>
int A<void>::B<U>::i = 1;
  • 可以用全特化代替对应的某个实例化体,但两者不能在一个程序中同时存在,否则会发生编译期错误
template<typename T>
class X {};

X<double> x; // 产生一个X<double>实例化体

template<>
class X<double>; // 错误:X<double>已经被实例化了
  • 如果在不同的编译单元出现这种情况很难捕捉错误,如果没有特殊目的应该避免让模板特化来源于外部资源。下面是一个无效的例子
// Translation unit 1:
template<typename T>
class X {
 public:
  enum { max = 10; };
};

char buffer[X<void>::max]; // 使用的max是10

extern void f(char*);

int main()
{
  f(buffer);
}

// Translation unit 2:
template<typename T>
class X;

template<>
class X<void> {
 public:
  enum { max = 100; };
};

void f(const char* buf)
{
  // 可能与原先定义的数组大小不匹配
  for (int i = 0; i < X<void>::max; ++i)
  {
    buf[i] = '\0';
  }
}

成员全特化

template<typename T>
class A {
 public:
  template<typename U>
  class B {
   private:
    static int x;
  };

  static int y;

  void print() const
  {
    std::cout << "generic";
  }
};

template<typename T>
int A<T>::y = 6;

template<typename T>
  template<typename U>
int A<T>::B<U>::x = 7;

// A<bool>的特化,对特化整个类模板可以完全改变类的成员
template<>
class A<bool> {
 public:
  template<typename U>
  class B {
   private:
    static int x;
  };

  void print() const {}
};

// 由于A<bool>中的B也是类模板,所以也可以特化
template<> // 特化的普通成员不需要加template<>前缀,但B是模板
class A<bool>::B<wchar_t> {
 public:
  enum { x = 2 };
};

// 下面是A<void>成员的特化,A<void>其他成员将来自原模板
// 成员特化后就不能再特化整个A<void>
template<>
int A<void>::y = 12;

template<>
void A<void>::print() const
{
  std::cout << "A<void>";
}
// template<> class A<void> {}; // 错误:不能再特化A<void>

// 特化A<wchar_t>::B
template<>
  template<typename U>
class A<wchar_t>::B {
 public:
  static long x; // 成员类型发生了改变
};

template<>
  template<typename X>
long A<wchar_t>::B<X>::x;

// 特化A<char>::B<wchar_t>
template<> // A<char>没被特化,所以比特化A<bool>::B<wchar_t>多一个前缀
  template<>
class A<char>::B<wchar_t> {
 public:
  enum { x = 1 };
};
  • 普通类的成员函数和静态成员变量在类外的非定义声明是非法的
class A {
  void f();
};

void A::f(); // 错误
  • 但成员特化可以在类外声明
template<typename T>
class A {
 public:
  template<typename U>
  class B {
   private:
    static int x;
  };

  static int y;

  void print() const
  {
    std::cout << "generic";
  }
};

template<>
int A<void>::y;

template<>
void A<void>::print() const;
  • 如果静态成员变量只能用默认构造函数初始化,就不能定义它的特化
class X {
 public:
  X() = default;
  X(const X&) = delete;
};

template<typename T>
class Y {
 private:
  static T i;
};

// 下面只是一个声明
template<>
X Y<X>::i;
// 下面是定义(C++11前不存在定义的方法)
template<>
X Y<X>::i{};
// C++11前
template<>
X Y<X>::i = X(); // 但这不可行,因为拷贝构造函数被删除了
// 但C++17引入了copy-elision规则又使得这个方法可行

类模板偏特化

  • 类模板偏特化限定一些类型而非某个具体类型
template<typename T>
class X {}; // 原始模板

template<typename T>
class X<const T> {}; // T为const的情况

template<typename T>
class X<T*> {}; // T为指针的情况

template<typename T, int N> // 参数个数可以和原始模板不一致
class X<T[N]> {}; // T为数组的情况

template<typename A>
class X<void* A::*> {}; // T为成员指针且成员返回类型为void的情况

template<typename T, typename A>
class X<T* A::*> {}; // T为成员指针的情况(如果为void则调用上一个偏特化)

template<int I, int N>
class S {};

template<int N>
class S<2, N> {}; // I为2的情况

template<typename... Ts>
class Tuple {}; // 原始模板

template<typename T>
class Tuple<T> {}; // tuple只有单个元素的情况

template<typename T1, typename T2, typename... Ts>
class Tuple<T1, T2, Ts...> {}; // tuple有两个以上元素的情况
  • 偏特化可能产生无限递归,解决方法是在偏特化前提供一个全特化,匹配时全特化会优于偏特化
template<typename T>
class X {};

template<>
class X<void*> {}; // 加上此全特化以防X<void*>产生无限递归

template<typename T>
class X<T*> {
 public:
  X<void*> x; // X<void*>中又包含X<void*>,将产生无限递归
};
  • 下面是一些错误的偏特化
template<typename T, int I = 3>
class X {}; // 原始模板

template<typename T>
class X<int, T> {}; // 错误:参数类型不匹配

template<typename T = int>
class X<T, 10> {}; // 错误:不能有默认实参(可以用原始模板的默认实参)

template<int I>
class X<int, I*2> {}; // 错误:不能有非类型表达式

template<typename U, int K>
class X<U, K> {}; // 错误:与原始模板相同

template<typename... Ts>
class Tuple {}; 

template<typename T, typename... Ts>
class Tuple<Ts..., T> {}; // 错误:包扩展必须在实参列表末尾

template<typename T, typename... Ts>
class Tuple<Tuple<Ts...>, T> {}; // OK:包扩展在嵌套模板实参列表末尾
  • 偏特化会匹配更特殊的版本,如果匹配程度一样就会出现二义性错误
template<typename T>
class X {};

template<typename T>
class X<const T> {};

template<typename T, int N>
class X<T[N]> {};

X<const int[3]> x; // 错误:两个偏特化匹配程度相同
  • 偏特化的类外成员定义和类模板的方式一样
template<typename T>
class X {};

template<typename T>
class X<T*> {
 public:
  void f();
};

template<typename T>
void X<T*>::f() {}

变量模板全特化

template<typename T>
constexpr std::size_t X = sizeof(T);

template<>
constexpr std::size_t X<void> = 0;
  • 变量模板特化的类型可以不匹配原始模板
template<typename T>
typename T::iterator null_iterator; // 一个空迭代器(但不是nullptr)

template<>
int* null_iterator<std::vector<int>> = nullptr; // 允许int*不匹配T::iterator

auto p = null_iterator<std::vector<int>>; // int* p = nullptr
auto q = null_iterator<std::deque<int>>; // std::deque<int>::iterator q = 空迭代器

变量模板偏特化

template<typename T>
constexpr std::size_t X = sizeof(T);

template<typename T>
constexpr std::size_t X<T&> = sizeof(void*);
  • 和变量模板的全特化一样,偏特化的类型不需要和原始模板匹配
template<typename T>
typename T::iterator null_iterator;

template<typename T, std::size_t N>
T* null_iterator<T[N]> = nullptr;

auto p = null_iterator<int[3]>; // int* p = nullptr
auto q = null_iterator<std::deque<int>>; // std::deque<int>::iterator q = 空迭代器
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容

  • 项目地址 C++类模板(1) 与函数模板类似,类也可以通过参数泛化,从而可以构建出一组不同型别的类实例(对象) 类...
    偷天神猫阅读 893评论 0 1
  • 01 一个实例:累加一个序列 1.1 Fixed Traits 上述代码的问题是,对于 char 类型希望计算对应...
    奇点创客阅读 275评论 0 0
  • 一、概述 二、模板定义与使用  1. 函数模板的定义与使用 2. 类模板的定义与使用 三、typename 的特殊...
    从不中二的忧伤阅读 1,634评论 1 1
  • 本文的标题改为陈述句可能更合适:为什么不该特化函数模板。 重载 v.s. 特化 为了更好的理解,我们先快速地回顾一...
    suesai阅读 2,200评论 0 0
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,518评论 28 53