[c++] Templates
远观像"类",近看不是
Template是编译时多态。所有的模板都是在编译时产生对应的代码,它没有面向对象中的虚表,无法实现动态多态。
模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。
Function Template
A function template is a prescription for the compiler to generate particular instances of a function varying by type.
”变量类型“的部分放在一块,与“变量类型无关”的部分放在另一边。
放在"头文件“中,声明和实现建议这么做。其他方式不推荐。
模板函数
typename的使用,增强了函数的扩展性。
#include <iostream> #include <string> using namespace std; ------------------------------------------------------------------- template <typename T> inline T const& Max (T const& a, T const& b) { return a < b ? b:a; }
-------------------------------------------------------------------
int main () { int i = 39; int j = 20; cout << "Max(i, j): " << Max(i, j) << endl; double f1 = 13.5; double f2 = 20.7; cout << "Max(f1, f2): " << Max(f1, f2) << endl; string s1 = "Hello"; string s2 = "World"; cout << "Max(s1, s2): " << Max(s1, s2) << endl; return 0; }
Name Resolution
概念解析:
Ref: Templates and Name Resolution
中文版链接:模板和名称解析
在模板定义中,有三种类型的名称。
-
-
-
本部声明的名称,包括模板本身的名称以及在模板定义中声明的任何名称。
-
模板定义之外的封闭范围中的名称。
-
在某种程度上依赖于模板自变量的名称 (称为依赖名称) 。
-
-
参考:《C++语言程序设计(第4版)》(郑莉,董渊)
-
- designer & client
- Only three kinds of implicit conversions for type parameters:
参数的类型不同的数字比较的话,例如:int , double
使用例如:min(static_cast<double> i, d)的方式。
decltype: what's its type?
decltype 与 auto
Ref: auto和decltype的用法总结
Ref: auto和decltype的用法总结【有赞】
在使用过程中和const、引用和指针结合时需要特别小心。decltype和auto都可以用来推断类型,但是二者有几处明显的差异:
- auto忽略顶层const,decltype保留顶层const;
- 对引用操作,auto推断出原有类型,decltype推断出引用;【decltype更牛逼一些】
- 对解引用操作,auto推断出原有类型,decltype推断出引用;
- auto推断时会实际执行,decltype不会执行,只做分析。
(1) decltype 的作用
decltype只是为了推断出表达式的类型而不用这个表达式的值来初始化对象.
表达式:
decltype(func()) sum = x; // sum的类型是函数func()的返回值的类型, 但是这时不会实际调用函数func()
变量:
int i = 0;
decltype(i) j = 4; // i的类型是int, 所以j的类型也是int
(2) decltype 和 const
不论是顶层const还是底层const, decltype都会保留
const int i = 3;
decltype(i) j = i; // j的类型和i是一样的, 都是const int
作为对比,auto的特性则是:
auto会忽略掉顶层const,保留底层const. 举例:
const int i = 5; auto a = i; // 变量i是顶层const, 会被忽略, 所以b的类型是int auto b = &i; // 变量i是一个常量, 对常量取地址是一种底层const, 所以b的类型是const int *
因此,如果希望推断出的类型是顶层const的,那么就需要在auto前面加上cosnt:
const auto c = i;
(3) decltype 和 引用
① 如果表达式是引用类型, 那么decltype的类型也是引用
const int i = 3, &j = i; decltype(j) k = 5; // k的类型是 const int &
② 如果表达式是引用类型, 但是想要得到这个引用所指向的类型, 需要修改表达式: 【找引用指代的原类型】
int i = 3, &r = i; decltype(r + 0) t = 5; // 此时是int类型
③ 对指针的解引用操作返回的是引用类型【找指针指代的原类型】
int i = 3, j = 6, *p = &i; decltype(*p) c = j; // c是int类型的引用, c和j绑定在一起
④ 如果一个表达式的类型不是引用, 但是我们需要推断出引用, 那么可以加上一对括号, 就变成了引用类型了【凭空添加一个原变量的引用类型】
int i = 3; decltype((i)) j = i; // 此时j的类型是int类型的引用, j和i绑定在了一起
为啥用 decltype
auto 和 decltype 都是编译时类型推导。
auto 解决不了参数type不定的问题
auto
means "the variable's type is deduced from the initialiser."
decltype
refers to a type in an arbitrary context.
Here's an example where you can't use auto
:
template <typename T, typename U, typename V> void madd(const T &t, const U &u, const V &v, decltype(t * u + v) &res) { res = t * u + v; }
(1) 解决模板函数返回值不确定情况:
template <typename T, typename U>
auto mymin(T a, U b) -> decltype(a < b ? a : b) {
return a < b ? a: b;
}
泛型编程中结合auto,用于追踪函数的返回值类型!
(2) 根据变量推导齐类型:
int i;
const &j = i;
int *p = i;
int k;
decltype(i) x; // int x: i is a variable
decltype(j) y = k; // int &y = k: j is an lvalue
decltype(*p) z = k; // int &z = k: *p is an lvalue
decltype((i)) w = k; // int &w = k: (i) is an lvalue
(3) 与using/typedef合用,用于定义类型:
Ref: C++11特性:decltype关键字
using size_t = decltype(sizeof(0));//sizeof(a)的返回值为size_t类型 using ptrdiff_t = decltype((int*)0 - (int*)0); using nullptr_t = decltype(nullptr);
vector<int >vec; typedef decltype(vec.begin()) vectype; for (vectype i = vec.begin; i != vec.end(); i++) { //... }
(4) 重用匿名类型:
在C++中,我们有时候会遇上一些匿名类型,而借助decltype,我们可以重新使用这个匿名的结构体:
struct
{
int d ;
doubel b;
}anon_s;
decltype(anon_s) as ; //定义了一个上面匿名的结构体
decltype推导四规则
推导规则
- 如果e是一个没有带括号的标记符表达式或者类成员访问表达式,那么的decltype(e)就是e所命名的实体的类型。此外,如果e是一个被重载的函数,则会导致编译错误。
- 否则 ,假设e的类型是T,如果e是一个将亡值,那么decltype(e)为T&&
- 否则,假设e的类型是T,如果e是一个左值,那么decltype(e)为T&。
- 否则,假设e的类型是T,则decltype(e)为T。
int decipline(void) { int i = 4; int arr[5] = { 0 }; int *ptr = arr; struct S{ double d; }s ; // void Overloaded(int); // void Overloaded(char);//重载的函数 int && RvalRef(); const bool Func(int); //规则一:推导为其类型 decltype (arr) var1; //int 标记符表达式【又是一个数组的bug】 decltype (ptr) var2; //int * 标记符表达式 decltype (s.d) var3; //doubel 成员访问表达式 //decltype(Overloaded) var4;//重载函数。编译错误。 //规则二:将亡值。推导为类型的右值引用。 decltype (RvalRef()) var5 = 1; //规则三:左值,推导为类型的引用。 decltype ((i))var6 = i; //int& decltype (true ? i : i) var7 = i; //int& 条件表达式返回左值。 { int x = 10; float y = 1.11; decltype(true?x:y) c = (false?x:y); cout << "c = " << c << endl; } decltype (++i) var8 = i; // int& ++i返回i的左值。 decltype(arr[5]) var9 = i; // int&. []操作返回左值 decltype(*ptr) var10 = i; // int& *操作返回左值 decltype("hello")var11 = "hello"; // const char(&)[9] 字符串字面常量为左值,且为const左值。 //规则四:以上都不是,则推导为本类型 decltype(1) var12;//const int decltype(Func(1)) var13=true;//const bool decltype(i++) var14 = i;//int i++返回右值 return 0; }
左值判断
cout << is_lvalue_reference<decltype(++i)>::value << endl;
模板的健壮性
(1) 若要比较字符串,使用函数override.
const char *s = "xyz";
const char *t = "abc";
/* 返回了数字,认为是地址,也就是数组的首地址 */
cout << min(s, t) << endl;
(2) 模板的设计 更加健壮。如下:
(3) template function的默认参数 也可以设置,实现健壮性:
(4) template's variadic function ,进一步健壮:
注意关键写法:
template <typename A, typename... B>
void print(A head, B... tail) {
...
}
Class Template
---- 类是不能重载的,要解决这个问题。
菜鸡教程
- 作为对比:函数模板
template <typename T> inline T const& Max (T const& a, T const& b) { return a < b ? b:a; }
- 类的模板
using namespace std;
template <typename> class Stack;
template <typename T> ostream& operator << (ostream &os, const Stack<T>&s);
template <typename T> class Stack {
public:
friend ostream& operator << <T>(ostream &, const Stack<T> &); //指明:是以上实现函数模板的实例
void push(const T &);
void pop();
T& top();
const T& top() const;
bool empty() const;
private:
vector<T> stack_; // 同时也表明了:在已有的类型的基础上 再扩展出一个类型。更加灵活的方式可使用参数法传入。
};
- 菜鸡实例
Ref: http://www.runoob.com/cplusplus/cpp-templates.html
一个栈的“类”,但不知道未来的元素可能会是什么类型,遂使用“模板”。
#include <iostream> #include <vector> #include <cstdlib> #include <string> #include <stdexcept> using namespace std; template <class T> // 【定义模板】 class Stack { private: vector<T> elems; // 元素 public: void push(T const&); // 入栈 void pop(); // 出栈 T top() const; // 返回栈顶元素 bool empty() const{ // 如果为空则返回真。 return elems.empty(); } };
---------------------------------------------------------------- template <class T> void Stack<T>::push (T const& elem) { // 追加传入元素的副本 elems.push_back(elem); } template <class T> void Stack<T>::pop () { if (elems.empty()) { throw out_of_range("Stack<>::pop(): empty stack"); } // 删除最后一个元素 elems.pop_back(); } template <class T> T Stack<T>::top () const { if (elems.empty()) { throw out_of_range("Stack<>::top(): empty stack"); } // 返回最后一个元素的副本 return elems.back(); } ----------------------------------------------------------------
int main() { try { Stack<int> intStack; // int 类型的栈 【使用模板】 Stack<string> stringStack; // string 类型的栈 // 操作 int 类型的栈 intStack.push(7); cout << intStack.top() <<endl; // 操作 string 类型的栈 stringStack.push("hello"); cout << stringStack.top() << std::endl; stringStack.pop(); stringStack.pop(); } catch (exception const& ex) { cerr << "Exception: " << ex.what() <<endl; return -1; } }
COMP6771 教程
模板的高一个层次的视角去看待这个问题:std::deque<T>
在头文件中实现:
友元相关的部分:
template <typename T>
std::ostream& operator<<(std::ostream &os, const Stack<T> &s) {
for (unsigned int i = 0; i < s.stack_.size(); i++) {
os << s.stack_[i] << " ";
}
return os;
}
//---test.h #ifndef test_h_ #define test_h_ #include <iostream> using namespace std; // 改动一:增加函数模板的声明——而这又需要先声明类模板 template <class T> class Test; template <class T> void display(Test<T> &t); template <class T> class Test { private: T x; public: Test (T x_): x(x_) {} friend void display<>(Test<T> &t); // 改动二:在函数名后面加上<>,指明它是之前声明的函数模板 的实例 }; template <class T> void display(Test<T> &t) { cout << t.x << endl; } #endif // test_h_
cpp中的使用:
// 类模板
Stack<int> s1; // int: template argument
知无涯之C++ typename的起源与用法
也许你会想到上面这段代码中的
typename
换成class
也一样可以,不错!那么这里便有了疑问,这两种方式有区别么?查看C++ Primer之后,发现两者完全一样。那么为什么C++要同时支持这两种方式呢?
既然
class
很早就已经有了,为什么还要引入typename
这一关键字呢?问的好,这里面有一段鲜为人知的历史(也许只是我不知道:-))。带着这些疑问,我们开始探寻之旅。Goto: 知无涯之C++ typename的起源与用法
问题浮现
在类作用域一节中,我们介绍了三种名称,由于MyClass
已经是一个完整的定义,因此编译期它的类型就可以确定下来,也就是说MyClass::A
这些名称对于编译器来说也是已知的。
可是,如果是像T::iterator
这样呢?T
是模板中的类型参数,它只有等到模板实例化时才会知道是哪种类型,更不用说内部的iterator
。通过前面类作用域一节的介绍,我们可以知道,T::iterator
实际上可以是以下三种中的任何一种类型:
-
-
- 静态数据成员
- 静态成员函数
- 嵌套类型
-
template <class T> void foo() { typename T::iterator * iter; // ... }
类模板遇上static关键字
马杀特小哥的简洁文字:c++类模板遇上static关键字
在c++中,我们不能把静态成员放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化,如下面的实例所示。
普通的类 的情况
不需要实例化对象就可以使用,而且类的所有对象都共享同一个静态类成员,因为类静态成员是属于类而不是对象。
include <iostream> using namespace std; class Obj{ public: // 声明类静态成员,这里不能对它进行初始化赋值 static int m_a; }; // 初始化类静态成员(通过范围解析符::) int Obj::m_a = 0; int main(){ // 通过对象访问静态属性 Obj o; o.m_a = 10; cout << o.m_a << endl; // 通过类访问静态属性 cout << Obj::m_a <<endl; }
类模板遇到static
类模板的实现机制是通过二次编译原理实现的。c++编译器并不是在第一个编译类模板的时候就把所有可能出现的类型都分别编译出对应的类(太多组合了),
而是在第一个编译的时候编译一部分,遇到泛型不会替换成具体的类型(这个时候编译器还不知道具体的类型),而是在第二次编译的时候再将泛型替换成具体的类型(这个时候编译器知道了具体的类型了)。
由于类模板的二次编译原理再加上static关键字修饰的成员,当它们在一起的时候实际上一个类模板会被编译成多个具体类型的类,
所以,不同类型的类模板对应的static成员也是不同的(不同的类),但相同类型的类模板的static成员是共享的(同一个类)。
#include <iostream> using namespace std; template<typename T> class Obj{ public: static T m_t; }; template<typename T> T Obj<T>::m_t = 0;
int main04(){
Obj<int> i1,i2,i3; i1.m_t = 10; i2.m_t++; i3.m_t++; cout << Obj<int>::m_t<<endl; ------------------------------------------ Obj<float> f1,f2,f3; f1.m_t = 10; f2.m_t++; f3.m_t++; cout << Obj<float>::m_t<<endl; ------------------------------------------- Obj<char> c1,c2,c3; c1.m_t = 'a'; c2.m_t++; c3.m_t++; cout << Obj<char>::m_t<<endl; }
Member Template
---- 例如:list想使用vector的数据初始化
模板类的模板成员函数
template <typename T, typename CONT = std::deque<T>> class Stack {
// addition declarations:
Stack() {} // must define default constructor
// template function inside template class.
template <typename T2, typename CONT2>
Stack(const Stack<T2, CONT2> &);
template <typename T2, typename CONT2>
Stack(Stack<T2, CONT2> &&);
template <typename Iter>
Stack(Iter b, Iter e);
template <typename T2, typename CONT2>
Stack& operator=(const Stack<T2,CONT2> &);
template <typename T2, typename CONT2>
Stack& operator=(Stack<T2,CONT2> &&);
template <typename Iter>
void assign(Iter b, Iter e);
}
Implementation
(构建func etc.)
如何使用? Client Code
float a[] = {1.1, 2.2, 3.3};
Stack<float> fs(a, a+3);
//注:使用deque实现的Stack.
End.