《C++11Primer》阅读随记 -- 十六、模板与泛型编程

第十六章 模板与泛型编程

函数模板示例:

template <typename T>
int compare(const T& v1, const T& v2){
	if( v1 < v2 ) return -1;
	if( v2 < v1 ) return 1;
	return 0; 
}

模板类型参数

compare 函数有一个模板类型参数(type parameter)。一般来说,我们可以将类型参数看作类型说明符,就像内置类型或类类型说明符一样使用。特别是,类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换:

// 正确:返回类型和参数类型相同
template<typename T>
T foo(T* p){
	T temp = *p;	
	// ...
	return temp;
}

类型参数前必须用关键字classtypename

// 错误:U 之前必须加上 class 或 typename
template<typename T, U> T calc(const T&, const U&);

在模板参数列表中,这两个关键字的含义相同,可以互换使用。一个模板参数列表中可以同时使用这两个关键字:

// 正确:在模板参数列表中,typename 和 class 没有什么不同
template<typename T, class U>
T calc(const T&, const U&);

非类型模板参数

除了定义类型参数,还可以在模板中定义非类型参数( nontype parameter )。一个非类型参数表示一个值而非一个类型。我们通过一个特定的类型名而非关键字 classtypename 来指定非类型参数

当一个模板呗实例化时,非类型参数呗一个用户提供的或编译器推断出的值所代替。这些值必须是常量表达式,从而允许编译器在编译时实例化模板

例如,可以编写一个 compare 版本处理字符串字面常量。这种字面常量是 const char 的数组。由于不能拷贝一个数组,所以我们将自己的参数定义为数组的引用。由于希望能比较不同长度的字符串子面常量,因此为模板定义了两个非类型的参数。第一个模板参数表示第一个数组的长度,第二个参数表示第二个数组的长度

template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M]){
	return strcmp(p1, p2);
}

template<unsigned N, unsigned M>
int compare(const char* p1[N], const char* p2[M]) {
    return strcmp(p1, p2);
}

int compare(const char (&p1)[3], const char (&p2)[4])

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

inline 和 constexpr 的函数模板

函数模板可以声明为 inlineconstexpr 的,如果非模板函数一样。inlineconstexpr 说明符放在模板参数列表之后,返回类型之前

// 正确:inline 说明符跟在模板参数列表之后
template <typename T> inline
T min(const T&, const T&);
// 错误:inline 说明符的位置不正确
inline template <typename T>
T min(const T&, const T&);

编写类型无关的代码

// 使用指针也正确的 compare 版本
template<typename T>
int compare(const T& v1, const T& v2){
	if(less<T>()(v1, v2)) return -1;
	if(less<T>()(v2, v1)) return 1;
	return 0;
}

原始版本存在的问题是,如果用户调用它比较两个指针,且两个指针为指向相同的数组,则代码的行为是未定义的。

类模板

实现StrBlob 的模板版本。命名为 Blob,意味着他不再针对 string。类似 StrBlob,我们的模板会提供对元素的共享访问能力。与类不同,我们的模板可以用于更多类型的元素。与标准库容器相同,当使用 Blob 时,用户需要指出元素类型

template <typename T>
class Blob{
public:
	typedef T value_type;
	typedef typename std::vector<T>::size_type size_type;
	// 构造函数
	Blob();
	Blob(std::initializer_list<T> il);
	// Blob 中的元素数目
	size_type size() const { return data->size(); }
	bool empty() const { return data->empty(); }
	// 添加和删除元素
	void push_back(const T& t) { data->push_back(t); }
	// 移动版本
	void push_back(T&& t) { data->push_back(std::move(t)); }
	void pop_back():
	// 元素访问
	T& back();
	T& operator[](size_type i);
private:
	std::shared_ptr<std::vector<T>> data;
	// 若 data[i] 无效,则抛出 msg
	void check(size_type i, const std::string& msg) const;
};

实例化类模板

Blob<int> ia;			// 空 Blob<int>
Blob<int> ia2 = {0, 1, 2, 3, 4, 5};

编译器会实例化出一个与下面定义等价的类

template <>
class Blob<int>{
	typedef typename std::vector<int>::size_type size_type;
	Blob();
	Blob(std::initializer_list<int> il);
	// ...
	int& operator[](size_type i);
private:
	std::shared_ptr<std::vector<int>> data;
	void check(size_type i, const std::string& msg) const; 
}

类模板的成员函数

如果在类外定义一个成员时,必须说明成员属于哪个类。而且,从一个模板生成的类的名字中必须包含其模板实参。当我们定义一个成员函数时,模板实参与模板形参相同。即对于 StrBlob 的一个给定的成员函数

ret-type StrBlob::member-name(parm-list)

对应的 Blob 的成员应该是:

template <typename T>
ret-type Blob<T>::member-name(parm-list)

check 和元素访问成员

template<typename T>
void Blob<T>::check(size_type i, const std::string& msg) const
{
	if( i >= data->size() )
		throw std::out_of_range(msg);
}

在类代码内简化模板类名的使用

当我们使用一个类模板时必须提供模板实参,但这一个规则有一个例外。在类模板自己的作用域中,我们可以直接使用模板名而不提供实参

// 若试图访问一个不存在的元素,BlobPtr 抛出一个异常
template<typename T>
class BlobPtr{
public:
	BlobPtr() : curr(0) {}
	BlobPtr(Blob<T>& a, size_t sz = 0):wptr(a.data), curr(sz){}
	T& operator*() const
	{
		auto p = check(curr, "dereference past end");
		return (*p)[curr];		//(*p) 为本对象指向的 vector
	}
	// 递增和递减
	BlobPtr& operator++();		// 前置运算符
	BlobPtr& operator--();
private:
	// 若检查成功,check 返回一个指向 vector 的 shared_ptr
	std::shared_ptr<std::vector<T>>
		check(std::size_t, const std::string&) const;
	// 保存一个 weak_ptr,表示底层 vector 可能被销毁
	std::weak_ptr<std::vector<T>> wptr;
	std::size_t curr;	// 数组中的当前位置
}:

在类模板外使用类模板名

// 后置:递增/递减对象但返回原址
template <typename T>
BlobPtr<T> BlobPtr<T>::operator++(int){
	// 无须检查
	BlobPtr ret = *this;
	++*this;
	return ret;
}

由于返回类型位于类的作用域之外,我们必须指出返回类型是一个实例化的 BlobPtr,它所用的类型与类实例化所用的类型一致。

类模板和友元

当一个类包含一个友元声明时,类与友元各自是否时模板时相互无关的。如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例。如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例

一对一友好关系

类模板与另一个( 类或函数 )模板间友好关系的最常见的形式是建立对应实例及其友元间的友好关系。例如,Blob 类应该将 BlobPtr 和一个类模板版本的 Blob 相等运算符定义为友元

为了引用( 类或函数 )模板的一个特定实例,我们必须首先声明模板自身。一个模板声明包括模板参数列表

// 前置声明,在 Blob 中声明友元所需要的
template <typename T> class BlobPtr;
template <typename T> class Blob;
template <typename T>
	bool operator==(const Blob<T>&, const Blob<T>&);

template <typename T>
class Blob{
	// 每个 Blob 实例将访问权限授予相同类型实例化的 BlobPtr 和相等运算符
	friend class BlobPtr<T>
	friend class operator==<T>
		(const Blob<T>&, const Blob<T>&);
};

友元的声明用 Blob 的模板形参作为他们自己的模板实参。因此,友好关系被限定在用相同类型实例化的 BlobBlobPtr 相等运算符之间

Blob<char> ca;	// BlobPtr<char> 和 operator==<char> 都是本对象的友元
Blob<int> ia;	// BlobPtr<int> 和 operator==<int> 都是本对象的友元

BlobPtr<char> 的成员可以访问 ca 的非 public 部分,但 caiaBlob 的任何其他实例都没有特殊访问权限。

通用和特定的模板友好关系

// 前置声明,在将模板的一个特定实例声明为友元时要用到
template <typename T> calss Pal;

class C	{ // C 一个普通的非模板类
	friend class Pal<C>;		// 用类 C 实例化的 Pal 是 C的一个友元
	// Pal2 的所有实例都是 C 的友元;这种情况无须前置声明
	template <typename T> 
	friend class Pal2;
};

template <typename T>
class C2 {	// C2 本身是一个类模板
	// C2 的每个实例都将相同实例化的 Pal 声明为友元
	friend class Pal<T>;		// Pal 的模板声明必须在作用域之内(外?)
	// Pal2 的所有实例都是 C2 的每个实例的友元,不需要前置声明
	template <typename X> friend class Pal2;
	// Pal3 是一个非模板类,他是 C2 所有实例的友元
	friend class Pal3;			// 不需要 Pal3 的前置声明
};

为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数

令模板自己的类型参数称为友元

新标准中,可以将模板类型参数声明为友元

template< typename Type >
class Bar {
	friend Type;	// 将访问权限授予用来实例化 Bar 的类型
	// ...
};

此处将用来实例化 Bar 的类型声明为友元。因此,对于某个类型名 FooFoo 将称为 Bar<Foo> 的友元,Sales_data 将成为 Bar<Sales_data> 的友元,以此类推。

模板类型别名

类模板的一个实例定义了一个类类型,与任何其他类类型一样,可以定义一个 typedef 来引用实例化的类

typedef Blob<string> StrBlob;

由于模板不是一个类型,我们不能定义一个 typedef 引用一个模板。即,无法定义一个 typedef 引用 Blob<T>

但是,新标准允许我们为类模板定义一个类型别名:

template<typename T>
using twin = pair<T, T>;
twin<string> authors;		// authors 是一个 pair<string, string>

一个模板类型别名是一族类的别名:

twin<int> win_loss;		// win_loss 是一个 pair<int, int>

就像使用类模板一样,当我们使用 twin 时,需要指出希望使用哪种特定类型的 twin

当我们定义一个模板类型别名时,可以固定一个或多个模板参数:

template<typename T>
using partNo = pair<T, unsigned>;
partNo<string> books;	// books 是一个 pair<string, unsigned>
partNo<Vehicle> cars;	// cars 是一个 pair<Vehicle, unsigned>

类模板的 static 成员

与任何其他类相同,类模板可以声明 static 成员

template<typename T>
class Foo{
public:
	static std::size_t count() { return ctr; }
	// ...
private:
	static std::size_t ctr;
};

模板参数

模板参数遵循普通的作用域规则。一个模板参数名的可用范围是在其声明之后,至模板声明或定义结束之前。与任何其他名字一样,模板参数会隐藏外层作用域中声明的相同名字。但是,与大多数其他上下文不同,在模板内不能重用模板参数名:

typedef double A;
template <typename A, typename B>
void f(A, a, B, b){
	A temp = a;	// temp 的类型为模板参数 A 的类型,而非 double
	double B;	// 错误:重声明模板参数 B
}

正常的名字隐藏规则决定了 Atypedef 被类型参数 A 隐藏。因此, temp 不是一个 double,其类型是使用 f 时绑定到类型参数 A 的类型。

由于参数名不能重用,所以一个模板参数名在一个特定模板参数列表中智能出现以此

// 错误:非法重用模板参数名 V
template <typename V, typename V> // ...

模板声明

模板声明必须包含模板参数

使用类的类型成员

typedef typename std::vector::size_type size_type;

我们用作用域运算符( :: ) 来访问 static 成员和类型成员。在普通( 非模板 )代码中,编译器掌握类的定义。因此它知道通过作用域运算符访问的名字时类型还是 static 成员。例如,如果写下 string::size_type,编译器有 string 的定义,从而知道 size_type 是一个类型。

但对于模板代码就有困难。例如,假定 T 是一个模板类型参数,当编译器遇到类似 T::mem 这样的代码时,它不会知道 mem 时一个类型成员还是一个 static 数据成员,知道实例化时才会知道。但是,为了处理模板,编译器必须知道名字是否表示一个类型。例如

T::size_type * p;

它需要知道我们是正在定义一个名为 p 的变量还是将一个名为 size_typestatic 数据成员与名为 p 的变量相乘

默认情况下, C++ 假定通过作用域运算符访问的是名字不是类型。因此,如果希望使用一个模板类型参数的类型成员,就必须显式地告诉编译器改名字是一个类型。使用关键字 typename 来实现

template <typename T>
typename T::value_type top(const T& c){
	if( !c.empty() )
		return c.back();
	else
		return typename T::value_type();
}

top 函数期待一个容器类型的实参,它使用 typename 指明其返回类型并在 c 中没有元素时生成一个值初始的元素返回给调用者

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

默认模板实参

// compare 有一个默认模板实参 less<T> 和一个默认函数实参 F()
template <typename T, typename F = less<T>>
int compare(const T& v1, const T& v2, F f = F()){
	if( f(v1, v2) ) return -1;
	if( f(v2, v1) ) return 1;
	return 0;
}

当用户调用这个版本的 compare 时,可以提供自己的比较操作,但这不是必须的

bool i = compare(0, 42);	// 使用 less, i 为 -1
// 结果依赖于 item1 和 item2 中的 isbn
Sales_data item1(cin), item2(cin);
bool j = compare(item1, item2, compareIsbn); 

模板默认实参与类模板

template <class T = int>
class Numbers{
public:
	Number(T v = 0):val(v){}
	// ...
private:
	T val;
};
Number<long double> lots_of_precision;
Numbers<> average_precision;	// 空 <> 表示希望使用默认类型

成员模板

一个类( 无论时普通类还是类模板 )可以包含本身是模板的成员函数。这种成员函数称为 成员模板 ( member template )。成员模板不能是虚函数

普通( 非模板 )类的成员模板

定义一个类,类似 unique_ptr 所使用的默认删除器类型。类似默认删除器,我们的类将包含一个重载的函数调用运算符,它接收一个指针并对此指针执行 delete。与默认删除器不同,我们的类还将在删除器被执行时打印一条信息。由于希望删除器适用于任何类型,所以我们将调用运算符定义为一个模板

// 函数对象类,对给定指针执行 delete
class DebugDelete{
public:
	DebugDelete(std::ostream& s = std::cerr):os(s){}
	// 与任何函数模板相同,T 的类型由编译器推断
	template<typename T> void operator()(T* p) const{
		os << "deleting unique_ptr" << std::endl;
		delete p;
	}
private:
	std::ostream& os;
};

由于调用一个 DebugDelete 对象会 delete 其给定的指针,我们也可以将 DebugDelete 用作 unique_ptr 的删除器。为了重载 unique_ptr 的删除器,我们在见括号内给出删除器类型,ibng提供一个这种类型的对象给 unique_ptr 的构造函数

// 销毁 p 指向的对象
// 实例化 DebugDelete::operator()<int>(int*)
unique_ptr<int, DebugDelete> p(new int, DebugDelete());
// 销毁 sp 指向的对象
// 实例化 DebugDelete::operator()<string>(string*)
unique_ptr<string, DebugDelete> sp(new string, DebugDelete());

本例中,我们声明 p 的删除器的类型为 DebugDelete,并在 p 的构造函数中提供了改类型的一个未命名对象

控制实例化

当模板被使用时才会进行实例化这一特性意味着,相同的实例可能出现在多个对象问及那种。当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中就都会有改模板的一个实例

在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。在新标准中。我们可以通过 显式实例化( explicit instantiation ) 来避免。

extern template declaration;	// 实例化声明
template declaration;			// 实例化定义

declaration 是一个类或函数声明,其中所有模板参数以被替换为模板实参

// 实例化声明与定义
extern template class Blob<string>;				// 声明
template int compare(const int&, const int&);	// 定义

当编译器遇到 extern 模板声明时,它不会再本文件中生成实例化代码。将一个实例化声明为 extern 就表示承诺再程序其他位置由该实例化的一个非 extern 声明( 定义 )。对于一个给定的实例化版本,可能由多个 extern 声明,但必须只有一个定义

由于编译器再使用一个模板时自动对其实例化,因此 extern 声明必须出现再任何使用此实例化版本的代码之前

// Application.cc
// 这些模板类型必须在程序其他位置进行实例化
extern template class Blob<string>;
extern template int compare(const int&, const int&);
Blob<string> sa1, sa2;	// 实例化会出现在其他位置
// Blob<int> 及其接收 initializer_list 的构造函数在本文件中实例化
Blob<int> a1 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
Blob<int> a2(a1);		// 拷贝构造函数在本文件中实例化
int i = compare(a1[0], a2[0]);		// 实例化出现在其他位置

文件 Application.o 将包含 Blob<int> 的实例及其接收 initializer_list 参数的构造函数和拷贝构造函数的实例。而 compare<int> 函数和 Blob<string> 类将不再本文件中进行实例化。这些模板的定义必须出现在程序的其他文件中

// templateBuild.cc
// 实例化文件必须为每个在其他文件中声明为 extern 的类型和函数提供一个 ( 非 extern )
// 的定义
template int compare(const int&, const int&);
template class Blob<string>;	// 实例化类模板的所有成员

函数模板显式实参

在某些情况下,编译器无法推断出模板实参的类型。其他一些i情况下,我们希望允许用户控制模板实例化。当函数返回类型与参数列表中任何类型都不相同时,这两种情况最常出现

指定显式模板实参

定义i一个 sum 的函数模板,它接收两个痌类型的参数。我们希望允许用户指定结果的类型。这样,用户就可以选择合适的精度

// 编译器无法推断 T1,  它未出现在函数参数列表中
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);

在本例中,没有任何函数实参的类型可以用来推断 T1 的类型。每次调用 sum 时调用者都必须为 T1 提供一个显式模板实参( explicit template argument )

提供显式模板实参的方式与定义类模板实例的方式相同。显式模板实参在尖括号中给出,位于函数名之后,实参列表之前

// T1 时显式指定的, T2 和 T3 时从函数实参类型推断而来的
auto val3 = sum<long long>(i, lng);		// long long sum(int, long)

尾置返回类型与类型转换

// 例子
template <typename It>
??? &fcn(It beg, It end){
	// 处理序列
	return *beg;		// 返回序列中一个元素的引用
}

我们并不知道返回结果的准确类型,但直到所需类型时所处理的而序列的元素类型

vector<int> vi = {1, 2, 3, 4};
Blob<string> ca = { "hi", "bye" };
auto& i = fcn(vi.begin(), vi.end());	// fcn 应该返回 int&
auto& s = fcn(ca.begin(), ca.end());	// fcn 应该返回 string&
// 尾置返回类型允许我们在参数列表之后声明返回类型
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{
	// 处理序列
	return *beg;	// 返回序列中一个元素的引用
}
// 为了使用模板参数的成员,必须用 typename
template <typename It>
auto fcn2(It beg, It end) ->
	typename remove_reference<decltype(*beg)>::type
{
	// 处理序列
	return *beg;	// 返回序列中一个元素的拷贝
}

模板实参推断和引用

template <typename T>
void f1(T&);	// 实参必须是一个左值
// 对 f1 的调用使用实参所引用的类型作为模板参数类型
f1(i);			// i 是一个 int, 模板参数类型 T 是 int
f1(ci);			// ci 是一个 const int; 模板参数 T 是 const int
f1(5);			// 错误:传递给一个 & 参数的实参必须是一个左值

template <typename T>
void f2(const T&); 	// 可以接收一个右值
// f2 中的参数是 const &, 实参中的 const 是无关的
// 在每个调用中, f2 的函数参数都被推断为 const int&
f2(i);		// i 是一个 int; 模板参数 T 是 int
f2(ci);		// ci 是一个 const int, 但模板参数 T 是 int
f2(5);		// 一个 const & 参数可以绑定到一个右值; T 是 int

从右值引用函数推断类型

当一个函数参数是右值引用时,正常绑定规则告诉我们可以传递给它一个右值。当我们这样做时,类型推断过程类似普通左值引用函数参数的推断过程。推断出的 T 的类型时该右值实参的类型

template <typename T>
void f3(T&&);
f3(42);		// 实参是一个 int 类型的右值; 模板参数 T 是 int

理解 std::move

std::move 是如何定义的

// 在返回类型和类型转换中也要用到 typename
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
	return static_cast<typename remove_reference<T>::type&&>(t);
}

首先,move 的函数参数 T&& 是一个指向模板类型的参数的右值引用。通过引用折叠,此参数可以与任何类型的实参匹配。特别是,我们既可以传递给 move 一个左值,也可以传递给它一个右值:

string s1("hi!"), s2;
s2 = std::move(string("bye!"));		// 正确:从一个右值引动数据
s2 = std::move(s1);					// 正确: 但在赋值后,s1 的值是不确定的

std::move 是如何工作的

在第一个赋值中,传递给 move 的实参是 string 的构造函数的右值结果 ---- string("bye!")。当向一个右值引用函数参数传递一个右值时,右实参推断处的类型为被引用的类型。因此,在 std::move(string("bye!")) 中:

  • 推断出的 T 的类型为 string
  • 因此,remove_referencestring 进行实例化
  • move 的返回类型是 string&&
  • move 的函数参数 t 的类型为 string&&

函数体返回 static_cast<string&&>(t)t 的类型已经是 string&&,于是类型转换什么都不做

对于第二个赋值,它调用了 std::move()。传递给 move 的实参是一个左值

  • 推断出的 T 的类型为 string& ( string 的引用,而非普通 string )
  • 因此,remove_reference<string&>type 成员是 string
  • move 返回的类型仍是 string&&
  • move 的函数参数 t 实例化为 string& &&,会折叠为 string&

因此这个调用实例化 move<string&>, 即 string&& move(string& t)

这个实例的函数体返回 static_cast<string&&>(t)t 的类型为 string&cast 将其转换为 string&&

可变参数模板

一个可变参数模板( variadic template ) 就是一个接收可变数目参数的模板函数或模板累。可变数目的参数被称为参数包( parameter packet )。存在两种参数包:模板参数包( template parameter packet ),表示零个或多个模板参数:函数参数包( function parameter packet ),表示零个或多个函数参数

用一个省略号来指出一个模板参数或函数参数表示一个包。在一模板参数列表中,class...typename... 指出接下来的参数表示零个或多个类型的列表

// Args 是一个模板参数包;rest 是一个函数参数包
// Args 表示零个或多个模板类型参数
// reset 表示零个或多个函数参数
template <typename T, typename... Args>
void foo(const T& t, const Args& ... rest);

声明了 foo 是一个可变参数函数模板,它右一个名为 T 的类型参数,和一个名为 Args 的模板参数包。

int i = 0; double d = 3.14; string s = "how now brown cow";
foo(i, s, 42, d);		// 包中有三个参数
foo(s, 42, "hi");		// 包中有两个参数
foo(d, s);				// 包中有一个参数
foo("hi");				// 空包

sizeof… 运算符

当我们需要知道包中有多少元素时,可以使用 sizeof... 运算符。

template<typename... Args>
void g(Args... args){
	cout << sizeof...(Args) << endl;	// 类型参数的数目
	cout << sizeof...(args) << endl;	// 函数参数的数目
}

模板特例化

定义函数模板特例化

当特例化一个函数模板时,必须为源模板中的每个模板参数都提供实参。为了指出正在实例化一个模板,应使用关键字 template 后跟一个空尖括号 ( <> )。空尖括号自何处我们将为原模板的所有模板参数提供实参

// compare 的特殊版本,处理字符数组的指针
template <>
int compare(const char* const& p1, const char* const& p2){
	return strcmp(p1, p2);
}

特例化成员而不是类

template <typename T>
struct Foo{
	Foo(const T& t = T()) : mem(t){}
	void Bar() { /* ... */ }
	T mem;
	// ...
};
template<>		// 正在特例化一个模板
void Foo<int>::Bar()	// 正在特例化 Foo<int> 的成员 Bar
{
	...
}

Foo<string> fs;	// 实例化 Foo<string>::Foo()
fs.Bar();		// 实例化 Foo<string>::Bar()
Foo<int> fi;	// 实例化 Foo<int>::Foo()
fi.Bar();		// 使用特例化版本的 Foo<int>::Bar()
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Artintel

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值