C++ Primer 笔记——模板与泛型编程

时间:2022-02-23 21:39:12

1.编译器用推断出的模板参数来为我们实例化一个特定版本的函数。

2.每个类型参数前必须使用关键字class或typename。在模板参数列表中,这两个关键字含义相同,可以互换使用,也可以同时使用。

template <typename T, class U>
void test(T i, U j)
{
}

3.除了定义类型参数,还可以在模板中定义非类型参数,一个非类型参数表示一个值而非一个类型,当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替,这些值必须是常量表达式。

template <unsigned M, unsigned N>
int compare(const char (&p1)[M], const char(&p2)[N])
{
return strcmp(p1, p2);
} compare("test", "abc"); // test被转换成M=5,abc被转换成N=4 p1就是test数组的引用,p2就是abc数组的引用

因为是固定的类型,所以没有办法当作普通的形参类型来使用,只能出现在其他形参里面,以下我写了个更好理解的例子:

template <typename T, int size>
void output(T (&p)[size])
{
for (int i = ; i < size; i++)
std::cout << p[i] << std::endl;
} int arr[] = { ,, }; // size=3 p是arr数组的引用
double arr1[] = { 1.2, 2.3 }; // size=2 p是arr1数组的引用
output(arr);
output(arr1);

4.一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或左值引用。绑定到指针或引用非类型参数的实参必须具有静态的生存期。我们不能用一个普通(非static)局部变量或动态对象作为指针或引用非类型模板参数的实参。

5.函数模板可以声明为inline或constexpr的,inline或constexpr说明符放在模板参数列表之后,返回类型之前。

6.当编译器遇到一个模板定义的时候,它并不生成代码。只有当我们实例化模板的一个特定版本时,编译器才会生成代码。为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。所以模板的头文件通常既包括声明也包括定义。

7.与函数模板不同,编译器不能为类模板推断模板参数类型,所以必须在模板名后的尖括号中提供额外信息。每个类模板的每个实例都形成一个独立的类。默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化。

template <typename T>
class test
{
public:
test();
test<T>& operator=(const test<T>& t) { this->m_vec = t.m_vec; return *this; }
test copy(const test& t) { test tmp; tmp.m_vec = t.m_vec; return tmp; } // 在类的内部可以简化使用类名 template <typename U> // 可以嵌套其他的模板函数
U add(U i)
{
return i++;
} std::vector<T> m_vec;
}; template <typename T> // 定义可以在类外
test<T>::test()
{
} test<std::string> t;
test<std::string> t1 = t;
t.add(); // 返回2

8.如果一个类模板包含一个非模板友元,则友元被授权可以访问所以模板实例。如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。

以下是一对一的友好关系,即类型必须相同。

template <typename> class test;

template <typename T>
bool operator==(const test<T>& t1, const test<T>& t2); // 为了引用函数模板,必须先前置声明 template <typename T>
class test
{
friend bool operator==<T> (const test<T>& t1, const test<T>& t2); // t1 t2必须是相同类型
public:
test(int count) { m_count = count; } private:
int m_count;
}; template <typename T>
bool operator==<T>(const test<T>& t1, const test<T>& t2)
{
return t1.m_count == t2.m_count;
} test<int> t1();
test<int> t2();
test<std::string> t2();
bool b = t1 == t2; // 正确
b = t1 == t2; // 错误
template <typename T> class testfriend;

template <typename T>
class test
{
friend class testfriend<T>;
private:
int m_count;
}; template <typename T>
class testfriend
{
public:
int get_count(const test<T>& t) { return t.m_count; }
}; test<int> t;
testfriend<int> tf;
tf.get_count(t); // 正确 testfriend<double> tf1;
tf1.get_count(t); // 错误

以下是通用的友好关系:

class test
{
template <typename U> friend class testfriend;
private:
int m_count;
}; template <typename T>
class testfriend
{
public:
int get_count(const test& t) { return t.m_count; }
}; test t;
testfriend<int> tf;
testfriend<double> tf1;
tf.get_count(t); // 正确
tf1.get_count(t); // 正确
template <typename T>
class test
{
friend class testfriend;
private:
int m_count;
}; class testfriend
{
public:
template <typename T> int get_count(const test<T>& t) { return t.m_count; }
}; test<int> t1;
test<double> t2;
testfriend tf;
tf.get_count(t1); // 正确
tf.get_count(t2); // 正确
template <typename T>
class test
{
template <typename U> friend class testfriend;
private:
int m_count;
}; template <typename U>
class testfriend
{
public:
template <typename T> int get_count(const test<T>& t) { return t.m_count; }
}; test<int> t1;
test<double> t2;
testfriend<std::string> tf;
tf.get_count(t1); // 正确
tf.get_count(t2); // 正确 testfriend<char> tf1;
tf1.get_count(t1); // 正确
tf1.get_count(t2); // 正确

以下是限定特定的实例为友元的情况:

template <typename U> class testfriend;    // 必须要前置声明

template <typename T>
class test
{
friend class testfriend<int>; // 限定了只有testfriend<int>才是友元,其他类型都不是
private:
int m_count;
}; template <typename U>
class testfriend
{
public:
template <typename T> int get_count(const test<T>& t) { return t.m_count; }
}; test<char> t1;
test<std::string> t2; testfriend<int> tf1;
tf1.get_count(t1); // 正确
tf1.get_count(t2); // 正确 testfriend<std::string> tf2;
tf2.get_count(t1); // 错误
tf2.get_count(t2); // 错误

9.在新标准中,我们可以将模板类型参数声明为友元。这个类型也可以使用内置类型。

template <typename T>
class test
{
friend T;
private:
int m_count;
}; class mytype
{
public:
template <typename T> int get_count(const test<T>& t) { return t.m_count; }
}; test<mytype> t;
mytype mt;
mt.get_count(t); // 正确

10.我们可以定义一个typedef来引用实例化的类。新标准也允许我们为类模板定义一个类型别名。

template <typename T>
class test
{
}; typedef test<int> int_test; // 正确
typedef test<T> T_test; // 错误 template <typename T> using twin = std::pair<T, T>; // 正确
template <typename T> using twin_int = std::pair<T, int>; // 正确
twin<int> point; // point是一个pair<int,int>
twin_int<std::string> id; // id是一个pair<std::string,int>

11.模板类也可以声明static成员,每个模板类实例都有自己的static成员实例。

template <typename T>
class test
{
public:
static int m_count;
static std::vector<T> m_vec;
}; // 以下两个是不同的静态对象
test<int>::m_count;
test<double>::m_count; test<int>::m_vec;

12.当我们希望通知编译器一个名字表示类型时,必须使用关键字typename,而不能使用class。

template <typename T>
void test(const T& t)
{
typename T::value_type *p; // 此时p被声明为指针
}

13.早期的C++标准只允许为类模板提供默认实参,新标准中我们可以为函数和类模板提供默认实参。如果一个类模板为其所有模板参数都提供了默认实参,而且我们希望使用这些默认实参,就必须在模板名之后跟一个空尖括号对。

template <typename T = int, typename F = std::less<T>>
class test
{
}; test<> t;
test<double> t1;

14.我们在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。

template <typename T>
class test
{
public:
template <typename U> void Add(U u);
}; template <typename T>
template <typename U>
void test<T>::Add(U u)
{
} test<int> t;
t.Add(1.1);

15.因为模板被使用时才会进行实例化,所以当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中就会有该模板的一个实例。在新标准中,我们可以通过显示实例化来避免这种开销。

// test.h

template <typename T> class test;

template <>
class test<int> // 定义
{
}; // main.cpp extern template class test<int>; // 声明 test<int> t;
  • 当编译器遇到extern模板声明时,它不会再本文件中生成实例化代码。将一个实例化声明为extern就表示承诺在程序其他位置有该实例化的一个非extern声明(定义)。对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。
  • 由于编译器在使用一个模板时自动对其实例化,因此extern声明必须出现在任何使用此实例化版本的代码之前。
  • 对每个实例化声明,在程序中某个位置必须有其显示的实例化定义。
  • 在一个类模板的实例化定义中,所有类型必须能用于模板的所有成员函数。

16.将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有const转换及数组或函数到指针的转换,其他情况会生成新的实例。

template <typename T>
T Add(T a, T b)
{
return a + b;
} long i = 1;
Add(i, 2); // 错误,2是int,不会被转换成long

但是普通实参不受此限制

template <typename T>
T Add(T a, long b)
{
return a + b;
} long i = ;
Add(i, ); // 正确

17.当我们无法推断实参类型时,必须提供一个显示模板实参。

template <typename T1, typename T2, typename T3 >
T1 Add(T2 a, T3 b)
{
return a + b;
} long i = ;
Add(i, ); // 错误
Add<long long>(i, ); // 正确,返回long long类型

显示模板实参按由左至右的顺序与对应的模板参数匹配,只有尾部(最右)参数的现实模板实参才可以忽略,而且前提是它们可以从函数参数推断出来。

template <typename T1, typename T2, typename T3 >
T3 Add(T1 a, T2 b)
{
return a + b;
} long i = ;
Add<long long>(i, ); // 错误
Add<long, int, long long>(i, ); // 正确

对于上述模板类型参数已经显示指定了得函数实参,可以进行正常的类型转换。

template <typename T1, typename T2, typename T3 >
T3 Add(T1 a, T2 b)
{
return a + b;
} Add<long, int, long long>(, ); // 正确,1被转换成long

18.我们可以用尾置返回类型来确定模板方法的返回类型。

template <typename It>
auto test(It beg, It end) -> decltype(*beg) // 返回元素的引用
{
return *beg;
}

如果我们不想返回一个引用类型,可以使用标准库的类型转换。

template <typename It>
auto test(It beg, It end) ->
typename std::remove_reference<decltype(*beg)>::type    // 注意要加上typename表示这是一个类型
{
return *beg;
}

标准库还提供了一下的类型转换:
C++ Primer 笔记——模板与泛型编程

19.当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值。

template <typename T>
void test(const T& t1, const T& t2)
{
} void func(void(*)(const int&, const int&)) {}
void func(void(*)(const double&, const double&)) {} func(test); // 错误,不知道该实例化哪一个
func(test<int>); // 正确

20.当一个函数参数是模板类型参数的一个普通(左值)引用时,绑定规则告诉我们,只能传递给它一个左值。如果实参是const,则T将被推断为const类型。如果实参类型是const T&,则实参可以是右值,T的类型推断结果不会是一个const类型。

template <typename T>
void test(T& t)
{
} template <typename T>
void consttest(const T& t)
{
} int i = ;
const int j = ; test(i); // 正确
test(); // 错误
test(j); // 正确,T是const int consttest(i); // 正确
consttest(); // 正确
consttest(j); // 正确,T是int

21.当一个函数参数是一个右值引用时,正常绑定规则告诉我们可以传递给它一个右值。通常我们不能直接定义一个引用的引用,但是通过类型别名或通过模板类型参数间接定义是可以的。

template <typename T>
void test(T&& t)
{
} test(); // 正确 T是int int i = ;
test(i); // 看起来i是左值,好像不合法,但是T被推断为int&,其实test被折叠成左值引用

22.如果我们间接的创建了一个引用的引用,则这些引用形成了“折叠”。

  • X& &, X& &&, X&& &都折叠成 X& 类型。
  • 类型X&& &&折叠成X&&

23.函数模板可以被另一个模板或者一个普通非模板函数重载。而且当多个重载模板对一个调用提供同样好的匹配时,应该选择最特例化的版本。

template <typename T>
void test(const T &t)
{
} template <typename T>
void test(T *t)
{
} int i = ;
test(&i); // 可以匹配test(const int*&) 也可以匹配test(int*) 但是为了不转换成const,编译器会选择后者 const int j = ;
test(&j); // 可以匹配test(const int*&) 也可以匹配test(const int*) 看起来很有歧义
// 当实际上test(const T &t)本质上可以匹配任何类型,而test(T *t)只能匹配指针类型,所以会选择最特例化版本即后者

24.对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本。

template <typename T>
void test(const T &t)
{
} void test(const int &t)
{
} int i = ;
test(i); // 选择非模板版本

25.考虑以下代码:

template <typename T>
void test(const T &t)
{
} template <typename T>
void test(T *t)
{
} void test(const std::string &t)
{
} test("Hello");
// test(const T &t) T被绑定到 char[6]
// test(T *t) T被绑定到const char
// test(const std::string &t) 要求从const char*到string的类型转换
  • 以上两个模板都提供了精确匹配,第二个模板需要进行一次数组到指针的转换,这种转换被认为是精确匹配的。
  • 非模板版本是可行的,但是要进行一次用户定义的类型转换,所以没有精确匹配那么好。
  • T*版本更加特例化,编译器会选择它。

26.可变参数函数通常是递归的。

template <typename T>
ostream &print(ostream &os, const T &t)
{
return os << t;
} template <typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest)
{
os << t << ","; // 打印第一个实参
return print(os, rest...); // 递归打印剩余实参
} print(std::cout, , , , ); // 对于最后一次调用,非可变参数模板比可变参数模板更特例化,编译器会选择非可变的

27.当我们特例化一个函数模板时,必须为原模板中的每个模板参数都提供实参。空尖括号指出我们将为原模板的所有模板参数提供实参。特例化的本质是实例化一个模板,而非重载它,因此特例化不影响函数匹配。

template <typename T>
void test(T &t)
{
} template <>
void test(int &t)
{
}

28.与函数模板不同,类模板的特例化不必为所有模板参数提供实参。我们可以指定一部分而非所有模板参数,或是参数的一部分而非全部特性。一个类模板的部分特例化本身是一个模板,使用它时用户还必须为那些在特例化版本中未指定的模板参数提供实参。

template <typename T>
class test
{
public:
void Add() {/* do other*/}
}; template <>
void test<int>::Add()
{
// do int
} test<double> t;
t.Add(); // do other test<int> t1;
t1.Add(); // do int