c++primer 个人学习总结-模板和泛型编程

/*

*声明:

*内容主要来自c++ primer以及作者个人的整理归纳

*非常高兴如果能够帮到你

*非常抱歉如果文章内容有误

*非常感激如果能够指出我的问题

*/

函数模板:模板定义以关键字 template 开始,后跟一个模板参数列表 (template parameter list),这是一个逗号分隔的一个或多个模板参数 (template parameter) 的列表,用小于号 (<) 和大于号 (>) 包围起来。

当使用模板时,我们隐式/显式地指定模板实参,将其绑定到模板参数上。

实例化函数模板:编译器可以根据函数实参的类型来推导模板实参的类型并绑定到模板参数上。自c++17开始,类模板也支持模板实参推断。对于绑定了模板实参的模板函数,可以说是编译器生成了一个模板函数的版本,称为模板的实例。

注:需要区分模板参数(类似函数中的形参),模板实参(类似函数中的实参),模板实例化(将函数/类模板变为了一个可用的函数/类)。

非类型模板参数:除了定义类型参数,还可以在模板中定义非类型参数 (nontype parameter)。一个非类型参数表示一个值而非一个类型。我们通过一个特定的类型名而非关键字 class 或typename 来指定非类型参数。当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替。这些值必须是常量表达式,从而允许编译器在编译时实例化模板。

最常用的非类型模板实参是整型常量,最重要的用途是编译器固定大小的数组。

inline和constexpr的函数模板:函数模板可以声明为inline或constexpr的,如同非模板函数一样。inline或constexpr说明符放在模板参数列表之后,返回类型之前。

编写类型无关的代码:要求模板中的函数参数是const的引用,函数体中的条件判断仅使用<运算符。前者保证函数可以用于不能拷贝的类型,并且避免了拷贝大对象时浪费太多时间。后者同样也是降低了函数对处理类型的要求。

模板编译:当编译器遇到一个模板定义时,它并不生成代码。只有当我们实例化出模板的一个特定版本时,编译器才会生成代码。当我们使用〈而不是定义) 模板时,编译器才生成代码,这一特性影响了我们如何组织代码以及错误何时被检测到。

通常,当我们调用一个函数时,编译器只需要掌握函数的声明。类似的,当我们使用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现。因此,我们将类定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放在源文件中。

模板则不同: 为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义。

模板和头文件:模板包含两种名字,那些不依赖于模板参数的名字和那些依赖于模板参数的名字。

当使用模板时,所有不依赖于模板参数的名字都必须是可见的,这是由模板的提供者来保证的。而且,模板的提供者必须保证,当模板被实例化时,模板的定义,包括类模板的成员的定义,也必须是可见的。

用来实例化模板的所有函数、类型以及与类型关联的运算符的声明都必须是可见的,这是由模板的用户来保证的。

通过组织良好的程序结构,恰当使用头文件,这些要求都很容易满足。 模板的设计者应该提供一个头文件, 包含模板定义以及在类模板或成员定义中用到的所有名字的声明。模板的用户必须包含模板的头文件,以及用来实例化模板的任何类型的头文件。

大多数编译错误在实例化期间报告:编译器会在三个阶段报告错误,第一个阶段是编译模板本身时,在这个阶段只检查最基础的语法错误,例如分号和变量名拼错。第二个阶段是编译器遇到模板使用时,此时仍不会进行太多检查。对于函数模板调用,会检查实参数目是否正确,参数类型是否匹配。第三个阶段是模板实例化时,只有这个阶段才能发现类型相关的错误。


类模板:

类模板的成员函数:类模板的成员函数本身是一个普通函数。但是,类模板的每个实例都有其自己版本的成员函数。因此,类模板的成员函数具有和模板相同的模板参数。因而,定义在类模板之外的成员函数就必须以关键字 template 开始,后接类模板参数列表。举个例子,template <typename T> ret-type Blob<T>::memberName(parm-list) { 定义 }。一个类模板的成员函数,不会在类模板进行实例化时就也进行实例化,只有在用到这个函数的时候才回去进行实例化。即使某种类型不能完全复合模板操作的要求,我们也可以用该类型实例化该类。

在类模板函数体内,我们已经进入类的作用域,因此在定义类模板成员函数时无须重复模板实参。如果不提供模板实参,则编译器将假定我们使用的类型与成员实例化所用类型一致。

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

模板类的前向声明:template<typename T> class a; 需要template<typename T>来表明a为模板,不需要提供模板实参。

友元小结:友元类型若为类,编译器只需要知道friendClass是一个类名即可。友元类型若为非成员函数,编译器需要前置声明类,再声明函数原型(参数必须为指针/引用,否则不知道参数的size应该多大,而一个指针的大小本身是固定的),最后声明友元。友元类型若为其他类的成员函数,其他类本身需要前向声明,其他类的参数类型也必须已知。

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

cpp 复制代码
template <typename T> class Pal;

class C {
    friend class Pal<C>; //用C实例化的Pal类是C的友元
    template <typename T> friend class Pal2; //Pal2的所有实例都是C的友元,此时无需前置声明
}
template <typename T> class C2 {
    friend class Pal<T>; //相同实例化的Pal才声明为友元
    template <typename X> friend class Pal2; //Pal2的所有实例都是C2的每个实例的友元
    friend class Pal3; //Pal3为非模板类,它是所有C2实例的友元,也不需要前置声明
}

令模板自己的类型参数成为友元:friend T; 对于某个类型名Foo,Foo就将成为Blob<Foo>的友元。虽然此时T一般为类或函数,但也允许这种与内置类型的友好关系。

类模板的static成员:每个类模板的实例都有自己的static成员实例。因此,与定义模板的成员函数类似,我们将static数据成员也定义为模板。(static数据成员此时不属于模板而属于模板实例)。类似其他成员函数,一个static成员函数也只有在使用到时才会实例化。


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

模板声明:模板声明中必须包含模板参数,与函数类型相同,声明中模板参数的名字不必与定义相同。一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置,出现于任何使用这些模板的代码之前。

使用类的类型成员:假定 T 是一个模板类型参数,当编译器遇到类似T: :mem 这样的代码时,由于它不知道T的具体类型,它不会知道 mem 是一个类型成员还是一个 static 数据成员, 直至实例化时才会知道。但是,为了处理模板,编译器必须知道名字是否表示一个类型。例如,假定T 是一个类型参数的名字,当编译器遇到如下形式的语句时:

T::size_type * p

它需要知道我们是正在定义一个名为p的变量还是将一个名为 size_type 的 static 数

据成员与名为 p 的变量相乘。

默认模板实参:就像我们能为函数参数提供默认实参一样,我们也可以提供默认模板实参 。在新标准中,我们可以为函数和类模板提供默认实参。与函数默认实参一样, 对于一个模板参数, 只有当它右侧的所有参数都有默认实参时,它才可以有默认实参。


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

类模板的成员模板:与类模板的普通函数成员不同,成员模板是函数模板。当我们在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。类模板的参数列表在前,后跟成员自己的模板参数列表:template<typename T_class> template<typename T_func>

A<T_class>::func(T_func a) { }。


控制实例化:当模板被使用时才会进行实例化这一特性意味着,相同的实例可能出现在多个对象文件中。当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中就都会有该模板的一个实例。在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。在新标准中,我们可以通过显式实例化(explicit instantiation) 来避免这种开销。一个显式实例化有如下形式:

cpp 复制代码
extern template declaration; //实例化声明
template declaration; //实例化定义,强制生成了实例化代码

其中declaration是一个类或者函数声明,其中所有模板参数已被替换为模板实参。当编译器遇到 extern 模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为 extern 就表示承诺在程序其他位置有该实例化的一个非 extern 声明 〈定义)。对于一个给定的实例化版本,可能有多个 extern 声明,但必须只有一个定义。由于编译器在使用一个模板时自动对其实例化, 因此 extern 声明必须出现在任何使用此实例化版本的代码之前。对每个实例化声明,在程序中某个位置必须有其显式的实例化定义。

注:不同于处理类模板的普通实例化,显式的实例化定义会实例化所有成员,即使我们不使用某个成员,它也会被实例化。因此,在一个类模板的实例化定义中,所有类型必须能用于模板的所有成员函数。


效率和灵活性:对模板设计者面对的设计选择,标准库智能指针类型做出了一个很好的展示。

cpp 复制代码
template <
    class T,
    class Deleter = std::default_delete<T> // 删除器的类型是第二个模板参数
>
class unique_ptr;

对unique_ptr,Deleter 是一个模板类型参数。std::unique_ptr<int, DeleterA> 和 std::unique_ptr<int, DeleterB> 是两种完全不同的类型。它们之间不能相互赋值或转换,即使它们都管理 int 指针。这种设计牺牲了运行时的灵活性。

cpp 复制代码
template <class T>
class shared_ptr;

对shared_ptr,Deleter 不是一个模板类型参数。shared_ptr 对象本身通常只包含一个指向控制块 (Control Block) 的指针和一个指向被管理对象的指针。控制块是一个在堆上分配的、独立于 shared_ptr 对象本身的内存区域。它存储了引用计数,弱引用计数,一个类型擦除的删除器。shared_ptr优点在于类型兼容,std::shared_ptr<MyClass> 这种类型,无论它是由 new MyClass() 创建的,还是由 new MyClass() 加上一个自定义删除器创建的,它们的类型都是完全一样的。它们可以相互赋值,可以存入同一个容器 std::vector<std::shared_ptr<MyClass>>。但缺点在于效率稍低。

unique_ptr会在编译时绑定删除器,而shared_ptr会在运行时才去绑定删除器。


模板实参推断:编译器通常不会对实参进行类型转换来匹配已有的模板实例,少数几种支持的类型转换包括顶层const转换,数组或函数指针转换。需要注意的是,如果形参是一个引用,则数组不会转换为指针,此时数组类型是否相同还需关注数组的大小。(注意,是在进行模板实参推断时类型转换才受限)

注:函数模板可以有用普通类型定义的参数,即,不涉及模板类型参数的类型。这种函数实参不进行特殊处理,它们正常转换为对应形参的类型。

函数模板显式实参:

cpp 复制代码
template<typename T1,typename T2>
T1 sum(T2 a);

当编译器看到sum函数被调用时,编译器可以根据实参来推导T2的类型,但是却无法推导T1的类型,此时就需要为T1提供显式模板实参。显式模板实参按由左至右的顺序与对应的模板参数匹配; 第一个模板实参与第一个模板参数匹配,第二个实参与第二个参数匹配,依此类推。只有尾部〈最右) 参数的显式模板实参才可以忽略,而且前提是它们可以从函数参数推断出来。对于用普通类型(非模板类型)定义的函数参数,允许进行正常的类型转换,出于同样的原因,对于模板类型参数已经显式指定了的函数实参,也进行正常的类型转换。

尾置返回类型和类型转换:

cpp 复制代码
template<typename It>
??? &fcn(It beg,It end){
    return *beg;
}

template<typename It>
fcn(It beg,It end)->decltype(*beg) {
    return *beg;
}

有时,如果我们要求用户显式指定模板实参会给用户带来额外的负担,而且没什么额外的好处。例如,此例中,我们知道函数应该返回*beg,而且知道我们可以用 decltype (*beg)来获取此表达式的类型。但是,在编译器遇到函数的参数列表之前,beg 都是不存在的。所以我们必须添加一个额外的模板参数并显式指定改模板实参。为了解决这样的问题,我们可以将返回类型尾置。由于尾置返回出现在参数列表之后,它可以使用函数的参数。

进行类型转换的标准库模板类:由于*beg返回的一定是元素的引用,如果我们想要得到元素的值的话,那么可以使用标准库的类型转换模板。这些模板定义在type_traits中。在本例中,我们可以使用remove_reference来获取元素类型。remove_reference有一个模板类型参数和一个名为type的public类型成员,使用此type成员即可表示元素的值类型。

函数指针和实参推断:当我们用一个函数模板初始化一个函数指针或为一个函数指针赋值,时,编译器使用指针的类型来推断模板实参。例如:

cpp 复制代码
template <typename T> int compare (const T&,const T&);
// pfl 指向实例 int compare (const int&,const int&)
int (*pfl) (const int&,const int&) = compare;

但是,也需注意,用函数指针中的参数类型来推断函数模板参数的话,其灵活性远不如用实参来推断函数模板参数。例如:

cpp 复制代码
// func 的重载版本;每个版本接受一个不同的函数指针类型
void func(int(*) (const string&,const String&);
void func(int(*) (const int&,const int&);
func(compare); // 错误: 使用compare 的哪个实例?

从左值引用函数实参推断类型:此时,如果实参为const,对应的T将被推导为const类型。如果一个函数参数的类型是const T&,正常的绑定规则告诉我们可以传递给他任何类型的实参。

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

引用折叠和右值引用参数:对于一个T&&类型的模板参数,我们可能认为将一个左值绑定到它上面是违法的,毕竟通常我们并不能将一个右值引用绑定到一个左值上。但是,c++在正常绑定工作基础上额外定义了两条规则允许这种绑定,这两个规则也是std::move正确工作的基础。

1.让我们以template<typename> void func(T&& a)作为例子。我们先指出,第一条规则作用于T的推断,然后第二条规则作用于最后a的类型的确定。规则1:如果实参是类型为 U 的右值,则T被推断为U,如果实参是类型为U的左值,则T被推断为 U&。

2.引用折叠:我们可以看到,当T被推导为U&时,会产生"引用的引用",即(U&) &&。C++在语法上不允许你直接写引用的引用,但在模板实例化的过程中,这是完全可能发生的。为了处理这种情况,C++规定了一套简单的引用折叠规则:只要"引用的引用"中出现了任何一个&(左值引用)那么最终的结果就会被"折叠"成一个&(左值引用)。只有当两边都是&&(右值引用)时,结果才会是&&(右值引用)。

这两条规则导致了两个重要结果:

1.如果一个函数形参是一个指向模板类型参数的右值引用类型(如,T&&),则它可以被绑定到一个左值上。

2.如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将被实例化为一个普通的左值引用参数 (T&)。

注:这两个规则暗示,我们可以将任意类型的实参传递给T&&类型的函数参数。这也就是我们的万能引用。

万能引用的一些问题:虽然万能引用非常强大,但也会将一些问题复杂化。一个事实就是,我们无法确认T的类型。假如函数存在一个T&&类型的函数参数,当我们在函数体内定义一个T类型的变量时,我们不知道T是U&还是U,这意味着我们可能在不知不觉的时候错误的定义了一个引用。尽管我们可以使用remove_reference来避免这样的问题,但是,如果你想写的函数 f 只是想根据传入的是左值还是右值,执行不同的逻辑,而不是进行完美转发,那么额外提供类型为const T&的重载版本是更清晰、更简单、也更安全的做法。当实参为左值时,由于精确匹配会选择const T&的版本,当实参为右值时,会选择T&&的版本。

理解std::move:虽然我们不能直接将一个右值引用绑定到一个左值上,但可以用move获得一个绑定到左值上的右值引用。由于move本质上可以接受任何类型的实参,因此我们不会惊讶于它是一个函数模板。标准库是这样定义move的:

cpp 复制代码
template <typename T>

typename remove_reference<T>::type&& move (T&& t)
{
    return static_cast<typename remove_reference<T>::type&&>(t);
}

这段代码很短,但其中有些微妙之处。首先,move 的函数参数 T&g是一个指向模板类型参数的右值引用。通过引用折叠,此参数可以与任何类型的实参匹配。虽然c++规定,不能隐式地将一个左值转换为右值引用,但我们可以用static_cast显式地将一个左值转换为一个右值引用。


转发:某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在此情况下, 我们需要保持被转发实参的所有性质, 包括实参类型是否是 const 的以及实参是左值还是右值。例如数据结构中的递归接口和实际上递归的实现。

cpp 复制代码
template <typename F, typename Tl, typename T2>
void flip1(F f, Tl tl, T2 t2)
{
    f(t2, t1);
}

如果我们只使用上面的代码就幻想能够解决问题,那么,假如f函数接受一个int& 类型的参数,当我们把实参赋给flip1函数时,显然,模板类型推导只会将对应类型推导为普通类型,f在函数中对引用类型变量做修改时实际上修改的是一个生命周期仅限于flip1函数的变量。

定义能保持类型信息的函数参数:为了通过flip函数传递一个引用, 我们需要重写函数, 使其参数能保持给定实参的"左值性"。更进一步,可以想到我们也希望保持参数的 const 属性。通过将一个函数参数定义为一个万能引用类型, 我们可以保持其对应实参的所有类型信息。而使用引用参数 (无论是左值还是右值) 使得我们可以保持 const属性,因为在引用类型中的 const 是底层的。如果我们将函数参数定义为 T1&&和 T2&g&,通过引用折叠就可以保持翻转实参的左值/右值属性。

注:只有值传递时const才会被忽略,而对于指针和引用传递的底层const则必须保留。

cpp 复制代码
template <typename F,typename T1>
void flip2(F f,T1 &&t1)
{
    f(t1);
}
f(int &&a){
    ;
}

右值引用本质:这个版本看起来好像解决了之前的问题,但遗憾的是,它仍有缺陷。在此之前,我们需要对右值引用的本质进一步明确。一个右值引用表现其实和左值引用完全一样(除了右值引用可以直接绑定到临时量上),它们的不同仅仅在于名字不同,而右值引用的作用也就在于根据这个名字的不同让程序员明确的知道他可以,或者说他应该窃取这个引用中的资源。那么,到这里,我们就可以明确,一个右值引用其实就是一个左值。例如int &&a=30; 在这条语句之后,a并不会马上被销毁,a有名字,可以取地址,也可以出现在赋值号的左侧,它完全符合左值的定义。打个比方,右值引用变量在初始化时通过窃取等号右侧的"右值"的资源背叛了右值,获得了自己的资源,变成了一个"成功"的左值。到这里,我也更加清楚c++这样设计右值引用的目的:右值引用只能用临时量或者std::move(左值)来初始化。因为c++并不希望你像使用一个左值引用那样来使用一个右值引用,虽然右值引用的实质可能和左值引用差不多,但是,c++只希望user关注右值引用的抽象含义,而不是它的实际上如何去实现。并且,我也更加明白了规则的强大,通过对右值引用设立几条规则,我们就能使一个实际上和左值引用相通的东西表现为完全不同的另一个东西!

注:通过static_cast得到的右值引用是一个真正的右值。并且为了让std::move能够正确工作,我们也必须这样设计,否则,右值引用的基石都将会崩塌。std::move没有任何意义。

既然已经明确右值引用实际上是一个左值,那么当我们调用flip2(f,a);时(a是一个右值或者std::move得到的右值引用时),t1被初始化为T1&&类型的参数,t1是一个左值,当我们在flip2的函数体内向把t1传参给a时,我们就犯了将左值传递给右值引用的错误,所以,flip2的版本也是错误的。

在调用中使用std::forward保持类型信息:forward函数模板是一个条件式的static_cast,它能保持变量原始的类型。类似 move, forward 定义在头文件 utility 中。与move 不同,forward必须通过显式模板实参来调用。通常情况下,我们使用 forward 传递那些定义为模板类型参数的右值引用的函数参数。通过其返回类型上的引用折叠,forward 可以保持给定实参的左值/右值属性。

cpp 复制代码
template <typename Type> intermediary (Type &&arg)

{
    finalFcn (std::forward<Type>(arg));
}

根据传入的右值和左值,我们分别对应得到T为普通类型和T为左值引用类型,在forward函数中,真正工作的语句是return static_cast<T&&>(arg);,显然通过引用折叠,在传入右值时,我们会将arg转变为右值引用类型(此处得到右值),传入左值时,arg通过引用折叠仍然为一个左值。


重载和模板:

核心问题:当编译器遇到一个函数调用,比如 f(args),而存在多个名为 f 的普通函数和函数模板时,应该选择哪个版本的 f?

复习重载决议的四部曲:

1.确定候选函数集:相关作用域中的所有同名函数。

2.确定可行函数集:找出可以调用的版本。

3.选择最佳匹配:根据匹配程度选择最好的版本。

4.最终检查:如果有唯一的最好版本,调用成功,如果有多个最好版本或一个版本都没有,调用失败。

对于模板,编译器会先推断出如果使用函数模板f来进行调用的话得到的模板实例,然后将这个模板实例加入可行函数集中(或者直接失败)来和其他函数竞争。

1.如果有多个模板可以推导出相同的模板实例的话,那么我们会看哪个版本的模板更加特化,更加和推导出来的类型接近。比如说:

template <typename T> void f(T*); 比 template <typename T> void f(T); 更特化,因为它只接受指针类型。

template <typename T> void f(const T&); 比 template <typename T> void f(T); 更特化(在某些情况下),因为它对const有要求。

template <typename T, typename U> void f(T, U); 比 template <typename T> void f(T, T); 更泛化,因为后者要求两个参数类型相同。

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

注:在定义任何函数之前,记得声明所有重载的函数版本。 这样就不必担心编译器由于未遇到你希望调用的函数而实例化一个并非你所需的版本。


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

packet),表示零个或多个函数参数。我们用一个省略号来指出一个模板参数或函数参数表示一个包。 在一个模板参数列表中,class...或typename...指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。在函数参数列表中,

如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。例如:

// Args 是一个模板参数包; rest 是一个函数参数包

// Args 表示零个或多个模板类型参数

// rest 表示零个或多个函数参数

template <typename T,typename...Args>

void foo(const T &t,const Args& ... rest);

可以看到Args&...这里使人相当困惑,Args不是本来就是参数包了吗,难道这是在进行参数包的嵌套?其实Args&...这里的...含义为展开参数包,第一个...才是声明参数包的意思。

sizeof运算符:可以用sizeof(Args),sizeof(rest)来获取包中参数的数目。

可变参数函数模板:略

包扩展(其实是包展开):对于一个参数包, 除了获取其大小外, 我们能对它做的唯一的事情就是扩展 (expand)它。当扩展一个包时,我们还要提供用于每个扩展元素的模式 pattern 。(pattern就是省略号左边的部分)扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式右边放一个省略号(....) 来触发扩展操作。包扩展也可以用于调用函数,如func(rest)...,就是对每个元素调用func函数。

转发参数包:看例子吧

cpp 复制代码
// 目标函数,有多个重载版本
void target(int& x) { 
    std::cout << "Called target(int&)" << std::endl; 
    x++;
}
void target(int&& x) { 
    std::cout << "Called target(int&&)" << std::endl; 
}

// 转发函数
template <typename... Args>
void wrapper(Args&&... args) {
    target(std::forward<Args>(args)...);
}

模板特例化:有时,通用模板实例化后得到的版本对于某个类型来讲或许不是最好的实现,此时,我们可以给通用模板提供一个特例化的模板实例化版本。

函数模板特例化:语法形式为语法:template<> 表示这是一个特化,后面跟着完整的、具体的函数签名。(不要使用模板参数,使用具体的类型!)

注:为了特例化一个模板,原模板的声明必须在作用域中。而且,在任何使用模板实例的代码之前,特例化版本的声明也必须在作用域中。

对于普通类和函数,丢失声明的情况(通常) 很容易发现,编译器将不能继续处理我们的代码。但是,如果丢失了一个特例化版本的声明,编译器通常可以用原模板生成代码。由于在丢失特例化版本时编译器通常会实例化原模板,很容易产生模板及其特例化版本声明顺序导致的错误,而这种错误又很难查找。

如果一个程序使用一个特例化版本, 而同时原模板的一个实例具有相同的模板实参集合,就会产生错误。但是,这种错误编译器又无法发现。因此,模板及其特例化版本应该声明在同一个头文件中。 所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。

类模板特例化:和函数模板特例化大概类似。但是,类模板可以进行类模板部分特例化,注意,一个类模板的部分特例化版本本身只是一个模板,不能当作类使用。(部分特例化也包括特例化成员函数)

注:函数模板特例化一般不建议使用,应选择直接重载函数。

相关推荐
Scott9999HH10 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
2401_8414956411 小时前
【操作系统】进程同步与互斥实验报告
c++·算法·操作系统·进程·并发·同步·互斥
fqbqrr11 小时前
2607C++,soui与安卓
c++·soui
码智社11 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海11 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖12 小时前
JDK 26 新特性详解
java·开发语言
马优晨13 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
a11177614 小时前
2FA 验证码生成器(github登录验证 app)
笔记·学习
我的xiaodoujiao15 小时前
API 接口自动化测试详细图文教程学习系列32--Allure测试报告2
python·学习·测试工具·pytest
fqbqrr15 小时前
2607C++,使用微软detours勾挂工具
c++