最近在Github上复现了一个渲染器render的项目:
Github链接:tiny render
我希望在博客上可以记录自己的学习过程,博客主要分为两大类:《原理篇》和《语法篇》。
原理篇则主要讲的是——实现渲染器过程中所需要的图形学渲染算法知识。
语法篇则主要讲的是——实现渲染器过程中使用到的C++的语法知识。
今天我们来康康C++中关于template的用法——
一、template的简单概念
template可以说是c++语言中最难理解的知识点之一。
- What does template mean?
Something that establishes or serves as a pattern.
建立或服务于一种模式
c++中的模板可以让你定义一个能应用于不同类型对象的行为。
这个功能听起来与宏特别地接近(请参考一个简单的宏——确定两个数字中较大的宏MAX)。
事实上,宏是不安全类型的,模板是安全类型的。
二、template的声明语法
声明的时候,template后面会有一个参数列表,这个参数列表包含了关键字 typename,这个typename的存在是用来定义模板参数的对象类型的,使其成为对象type的占位符,上句话说的对象,就是模板正在实例化的那个对象。
template <typename T1, typename T2 = T1>
// A template function
bool TemplateFunction (const T1 ¶m1, const T2 & param2);
// A template class
template <typename T1, typename T2 = T1>
class MyTemplate
{
private:
T1 member1;
T2 member2;
public:
T1 GetObj()
{
return member1;
}
//...other members
};
上述code中,行3 4是一个template function,剩下的是一个template class。二者都采用了2个template参数——T1和T2,其中T2的类型已经被默认指定为T1。
三、不同类型的模板声明
一个模板声明可以是以下任何一个:
- 一个函数的声明或定义
- 一个类的声明或定义
- 一个成员函数或者一个类模板的成员类的定义
- 一个类模板的静态数据成员的定义
- 一个嵌套在类模板内的类的静态数据成员的定义
- 一个类或类模板的成员模板的定义
四、Template 函数
想象一个函数,它可以自我调整以适应不同类型的参数。这种函数一般很可能就是使用了template语法定义的。
让我们分析一个示例模板声明,它相当于前面讨论的宏MAX,返回两个提供的参数中较大的那个:
template <typename objType>
const objType& GetMax (const objType& value1, const objType& value2)
{
if (value1 > value2)
return value1;
else
return value2;
}
// sample usage one:
int num1 = 25;
int num2 = 40;
int maxVal = GetMax <int> (num1, num2);
// sample usage two:
double double1 = 1.1;
double double2 = 1.001;
double maxVal = GetMax <double> (double1, double2);
大家注意一下,在对调用时的细节。
这个细节有效地将模板参数定义为。
上面的代码让编译器生成了模板函数的两个版本,可以看作如下所示:
const int& GetMax (const int& value1, const int& value2)
{
//...
}
const double& GetMax (const double& value1, const double& value2)
{
//...
}
然而,实际上,模板函数并不一定需要一个相应的类型说明符。所以,下面的function写法可以实现地更好:
int maxVal = GetMax (num1, num2); // 在GetMax后面去掉了<int>
在这种case下,编译器是非常聪明的,可以明白这个模板函数是为整数类型调用的。
但是,对于模板类而言,大家需要显式地指明type!!
// A template function GetMax that helps evaluate the higher of two supplied values
#include <iostream>
#include <string>
using namespace std;
// 到模板的声明和一系列定义了
template <typename Type>
const Type& GetMax (const Type& value1, const Type& value2)
{
if (value1 > value2)
return value1;
else
return value2;
}
template <typename Type>
void DisplayComparison (const Type& value1, const Type& value2)
{
cout << "GetMax(" << value1 << ", " << value2 << ") = ";
cout << GetMax(value1, value2) << endl;
}
int main()
{
// int类型
int num1 = -101, num2 = 2011;
DisplayComparison(num1, num2);
// double类型
double double1 = 3.14, double2 = 3.1416;
DisplayComparison(double1, double2);
// string类型
string name1("Steven"), name2("Jessica");
DisplayComparison(name1, name2);
return 0;
}
上面这个示例有2个模板函数:和。
main()函数中30 34 38行说明了:相同的模板函数是如何被重复用于3个不同的的数据类型的:这3个数据类型分别是:。
这些模板函数不仅可以重复使用(就像它们的宏副本一样),而且它们还更容易编程和维护,并且是类型安全的!
当然,上述code中,大家用显式类型调用函数没问题的哈~,就像下面这样——
DisplayComparison <int> (num1, num2);
大家只需要记住一点:
- 编写模板函数时,可以不显式。但是编写模板类的时候,是需要显式的。
五、模板和类型安全
第四章的栗子中的模板函数都是type safe的。这意味着这两个模板函数不允许有毫无意义的调用:
DisplayComparison (num1, name1);
如果写成这样,将会立即导致编译失败。(num1和name1是不同数据类型的)
六、模板类
类是封装某些属性,和可以对这些属性进行操作的方法的编程单元。
属性通常是私有成员,例如中的。(年龄就是人的一种属性)
类可以被看作是一个宏伟的设计蓝图,类的实际表现形式是类的对象。
- 为什么要使用模板类呢?
例如,Tom可以被看作是的一个对象,它的属性年龄的值是15。如果出于应用程序的特定原因,需要你将年龄存储为自出生以来的秒数,如果是针对这种情况,将数据定义为型应该是不够的。为了程序的安全考虑,大家可能会想到用来代替。
那么碰到这个情况的时候,模板类就要派上用场了。
模板类是c++类的模板化版本。也可以说是设计蓝图的设计蓝图。
- 优点:当你在使用模板类时,你是可以指定要专门化该类的类型的。
这个优点可以让你,创造一些人的classes,这些class的模板参数可以是,一些使用,一些使用的整数。
一个简单的模板类,它可以使用一个单一的参数,来保存一个成员变量。具体实现如下:
template <typename T>
class HoldVarTypeT
{
private:
T value;
public:
void SetValue (const T& newValue)
{
value = newValue;
}
T& GetValue()
{
return value;
}
}
上面这个示例中,变量的类型是,这个类型是在模板被使用的时候,也就是被实例化的时候给变量分配的。这个成员变量,就是存储在参数中的。
那么我们往后学,大家看一下下面这个模板类的具体应用:
HoldVarTypeT <int> holdInt; // int类型的模板实例化
holdInt.SetValue(5);
cout << "The value stored is: " << holdInt.GetValue() << endl;
就是一个模板类,它指定为一个int类型的成员变量。
也就是说,现在大家已经可以使用这个模板类来保存和检索int类型的对象。
也就是说,模板类是为int类型的模板参数实例化的。类似地,大家也可以使用相同的类以类似的方式处理字符串。
HoldVarTypeT <char*> holdStr; // 这个<char*>是什么鬼?
holdStr.SetValue("Sample string");
cout << "The value stored is: " << holdStr.GetValue() << endl;
这次则是指定类型为 char型指针。
从上面的代码可以看到,模板类为类定义了一种模式并且帮助模式在模板可能使用的不同数据类型上实现该模式。(Thus, the template class defines a pattern for classes and helps implement that pattern on different data types that the template may be instantiated with.)
- 模板类可以用其他类型实例化,而不是像int或标准库提供的类这样的简单类型。
- 你可以使用自己定义的类实例化模板。
- 例如,当你在添加code定义模板类HoldVarTypeT的时,你可以通过向main()添加以下代码来实例化的模板:
HoldVarTypeT<Human> holdHuman;
holdHuman.SetValue(firstMan);
holdHuman.GetValue().IntroduceSelf(); //这行没太看懂。。。
上面的第一行代码就是说,HoldVarTypeT是一个模板类,Human是一个class,class作为template的参数,来定义变量holdHuman。(也就是说,class也可以作为template的参数,从而来实例化类Human的模板!)
七、声明具有多个参数的模板template
- 如何声明多个参数?
可以展开模板参数列表来声明由逗号分隔的多个参数。
因此,如果你希望声明一个泛型类,该类包含一对可以是不同类型的对象,你可以使用以下示例中所示的结构来实现这一点(显示一个带有两个模板参数的模板类):
template <typename T1, typename T2>
class HoldsPair
{
private:
T1 value1;
T2 value2;
public:
// ctor that initializes member variables
HoldsPair (const T1& val1, const T2& val2)
{
Value1 = val1;
value2 = val2;
};
// ... other member functions
};
在上面这个栗子中,接受了两个模板参数,分别是。大家可以使用这个来保存2个相同类型or不同类型的对象,如下所示:
// A template instantiation that pairs an int with a double
// 一个模板实例化,它可以将一个int 型与一个double型配对的模板实例化
HoldsPair <int, double> pairIntDouble (6, 1.99);
// A template instantiation that pairs an int with an int
// 一个模板实例化,它可以将一个int型与一个int型配对的模板实例化
HoldsPair <int, int> pairIntDouble (6, 500);
// 这个pairIntDouble是什么啊??
上面这个示例中,第一行中,T1指定类型为int,T2指定类型为double。
第二行中,T1和T2的类型均是int类型。
八、使用默认参数声明模板
大家可以修改之前版本的将int型声明为默认模板参数类型。
template <typename T1 = int, typename T2 = int>
class HoldsPair
{
// ...method declarations 类中的方法声明
}
这在构造上类似于定义默认输入参数值的函数,不同之处在于,在这种情况下,我们定义了默认类型。(This is similar in construction to functions that define default input parameter values
except for the fact that, in this case, we define default types.)
因此,可以将的第二种用法压缩为:
// Pair an int with an int (default type)
HoldsPair <> pairInts (6, 500); // 这个pairInts是什么啊?
也就是说,<> 里面可以什么都不填。
九、示例模板 class<> HoldsPair
现在是进一步开发到目前为止已经介绍过的模板版本的时候了。看看下面的栗子:
// 一个模板类具有一对成员属性
#include <iostream>
using namespace std;
// template with default params: int & double
// 具有默认参数的模板:int和double
template <typename T1 = int, typename T2 = double>
class HoldsPair
{
private:
T1 value1;
T2 value2;
public:
HoldsPair (const T1& val1, const T2& val2) //ctor
: value1(val1), value2(val2) {}
// 访问函数
const T1& GetFirstValue () const
{
return value1;
}
const T2& GetSecondValue () const
{
return value2;
}
};
int main()
{
HoldsPair<> pairIntDb1 (300, 10.09);
HoldsPair<short, const char*> pairShortStr (25, "Learn templates, love C++");
cout << "The first object contains -" << endl;
cout << "Value 1: " << pairIntDb1.GetFirstValue () << endl;
cout << "Value 2: " << pairIntDb1.GetSecondValue () << endl;
cout << "The second object contains -" << endl;
cout << "Value 1: " << pairShortStr.GetFirstValue () << endl;
cout << "value 2: " << pairShortStr.GetSecondValue () << endl;
return 0;
}
上述栗子中,访问器函数可以被用来去查询对象持有的值。
请注意上述栗子中,是如何基于模板实例化语法对进行调整,以返回适当的对象类型。
写到这里,大家已经设法在中定义了一个模式,可以重用该模式为不同的变量类型交付相同的逻辑。
因此,可以说,template的出现提高了代码的可重用性。
十、模板实例化和特例化
大家一定还记得,我们前面一直提到这么一句话——
- A template class is a blueprint of a class
因此,在编译器以某种形式使用之前,它并不真正存在。(它就只是一张蓝图而已)
也就是说,就编译器而言,大家定义但不使用的模板类(template class)是会被简单忽略的代码。
但是,一旦大家实例化了一个模板类(实例化就是对这个模板类指定了类型,用这个模板类定义了变量)。
比如,通过提供以下这样的模板参数——
HoldsPair <int, double> pairIntDb1;
那么,就相当于,你指示编译器使用模板为你创建了一个类,并将其实例化为指定的模板参数的类型(在本例中为)。
因此,
对于模板而言,实例化是使用一个或多个模板参数创建一个特定类型的行为或过程。
另一方面,在使用特定类型实例化时,可能会出现需要让你显式定义一个(不同)的模板的行为的情况。
这是你专门化一个类型的模板(或其行为)的地方。
当使用两个模板参数进行实例化时,模板类的特例化是下面这样:
template <> class HoldsPair <int, int>
{
// implementation code here
}
// template和class写在了一行
// class后面加上了<int, int>
显而易见的是,专门处理模板的代码必须遵循模板定义。
下面给大家看个栗子,这个栗子的核心是一个模板的特例化,演示了一个专门化版本和一个模板特例化后有多大的区别。(an example of a template specialization that demonstrates how different a specialized version can be from the template it specializes.)
// Demonstrates Template Specialization
#include <iostream>
using namespace std;
template <typename T1 = int, typename T2 = double>
class HoldsPair
{
private:
T1 value1;
T2 value2;
public:
HoldsPair(const T1& val1, const T2& val2) //ctor
: value1(val1), value2(val2) {}
// Accessor functions
const T1 & GetFirstValue() const;
const T2 & GetSecondValue() const;
};
// Specialization of HoldsPair for types int & int here.
template <> class HoldsPair <int, int>
{
private:
int value1;
int value2;
string strFun;
public:
HoldsPair (const int& val1, const int& val2) //ctor
: value1(val1), value2(val2) {}
const int & GetFirstValue() const
{
cout << "Returning integer " << value1 << endl;
return value1;
}
};
// 为什么return的是value1呢?value1不是都在private中都定义好了?
int main()
{
HoldsPair <int, int> pairIntInt(222, 333);
pairIntInt.GetFirstValue();
return 0;
}
// pairIntInt应该是个对象或者说变量
- 代码分析:(其实这里还是没有太明白二者的一个区别!)
显然,大家可以比较一下,在这个栗子和上个栗子中类的行为,你会注意到栗子中的template的行为是完全不一样的。
事实上,函数在模板对于的实例化中已经被更改,也显示了输出。
行20~36就是特例化的代码,表示了这个版本还有一个字符串的数据成员。这个字符串的数据成员在原始的模板定义中是没有的。
其实,原始模板定义中,甚至没有提供Accessor functions的执行方法,也就是和二者的实现,但是program仍然可以编译。
大家对这点是否有好奇?
原因是编译器只是需要template对于的实例化,为此,我们提供了一个足够完整的特例化实现。也就是说,这个示例不仅演示了模板特例化,而且还演示了编译器如何根据模板的使用来决定模板代码是考虑使用或者说忽略。
十一、模板类和静态数据成员
大家好,我们在之前学习了代码在template中,当编译器开始使用后,code是如何开始存在于编译器中的。
那么有人会问了,静态成员属性函数是如何存在于模板类中的?
在C++中,声明一个类的静态成员会导致这个静态成员会在类的所有实例之间共享。无论是类还是模板类,二者都是相似的,
除了一点——静态成员会被一个模板类中的所有对象共用,还是以同样的模板实例化的方式。
因此,我们可以说,假设在一个模板类中有一个静态数据成员,这个在一个针对于的类的所有实例中,也是静态的。
同样地,在类的所有实例中,也是静态的,并且是独立于的其他模板实例的。
换句话说,你可以将其视为编译器在一个模板类中创建静态成员变量的两个版本:是针对于的模板实例化,是针对于double的模板实例化。
好,那么下来,我们来看看静态变量对模板类及其实例的影响——
//------The Effect of Static Variables on Template Class and Instances Thereof------
#include <iostream>
using namespace std;
template <typename T>
class TestStatic
{
public:
static int staticVal;
};
// static member initialization
template <typename T> int TestStatic<T>::staticVal; //这句没看懂啊
int main()
{
TestStatic<int> intInstance;
cout << "Setting staticVal for intInstance to 2011" << endl;
intInstance.staticVal = 2011;
TestStatic<double> dbInstance;
cout << "Setting staticVal for Double_2 to 1011" << endl;
dbInstance.staticVal = 1011;
cout << "intInstance.staticVal = " << intInstance.staticVal << endl;
cout << "dbInstance.staticVal = " << dbInstance.staticVal << endl;
return 0;
}
上述code中,行20和行24中,将成员分别作为类型和类型的模板的实例化成员。
编译器在明明是两个不同的静态成员但是名字都是的情况下,依然将两个不一样的数值存储了进去。所以说,编译器确保静态变量的行为对于特定类型的模板类的实例化是保持不变的。
- 注意:模板类的静态成员实例化语法——
template <typename T> int TestStatic<T>::staticVal;
// This follows the pattern
template<template parameters> StaticType
ClassName<Template Arguments>::StaticVarName;
十二、可变模板(可变参数模板)
假设,现在大家想要编写一个添加了两个值的泛型函数。模板函数Sum()就实现了这一点——
template <typename T1, typename T2, typename T3>
void Sum (T1& result, T2 num1, T3 num2)
{
result = num1 + num2;
return;
}
添加两个值是很简单的。
但是,如果需要你只写一个函数,但是这个函数可以添加任意数量的数值,每个都可以传递一个参数,那么你可能就要用到来定义这个函数了。
下面给出一个栗子——在定义一个函数时使用变量模板。
// 函数使用可变参数模板演示变量参数
// Function Using Variadic Templates Demonstrates Variable Arguments
#include <iostream>
using namespace std;
template <typename Res, typename ValType>
void Sum(Res &result, ValType& Val)
{
result = result + val; // 为啥这个result在=左右两边都有
}
template <typename Res, typename First, typename... Rest>
void Sum (Res& result, First val1, Rest... valN)
{
result = result + val1;
return Sum (result, valN ...); //明明是void为啥还有返回值 return
//这里体现了 递归 吗?
}
int main()
{
double dResult = 0;
Sum (dResult, 3.14, 4.56, 1.1111);
cout << "dResult = " << dResult << endl;
// 我想着结果的话应该是Sum,用dResult是因为dResult用的是引用传递的吗?
string strResult;
Sum (strResult, "Hello ", "World");
cout << "strResult = " << strResult.c_str() << endl;
return 0;
}
上述栗子中,函数使用可变模板不仅仅完整地处理了不同的参数类型(见行23、28),还处理了不同数量的参数。行23中调用的使用了4个参数,然而在行28中使用了3个参数,其中一个参数是,剩下的两个类型是。在编译期间,编译器实际上为了适当地调用正确类型的Sum()函数,创建了代码,递归执行(哪里递归了??),直到处理完所有参数。
- 有人会提问,上述栗子中使用了省略号...
- 省略号...的使用是告诉编译器模板类或者函数可以接受任意数量的任意类型的模板参数。
变量模板是c++的一个强大的补充,它可以应用于数学处理以及完成某些简单的任务。使用变量模板的程序员可以省去在各种重载版本中实现执行任务的函数的重复工作,从而创建更短、更容易维护的代码。
- C++ 14 提供了一个运算符,它可以告诉你在调用变量模板时传递的模板参数的数量。
- 在上面的栗子中,可以在Sum()这样的函数中使用这个操作符,就像下面的栗子
int arrNums[sizeof...(Rest)];
// 在编译时使用sizeof…()计算数组的长度
当然,大家不能把sizeof()和sizeof(Type)混淆哦。后者返回类型的大小,而前者返回发送给可变参数模板的模板参数的数量。
变量模板的支持还引入了对元组的标准支持。是实现元组的类模板。它可以用不同数量的成员元素及其类型完成实例化。还可以使用标准库函数单独访问它们。
下面大家一起来看看的实例化和使用。
// Instantiating and Using a std::tuple
// 实例化和使用std::tuple
#include <iostream>
#include <tuple>
#include <string>
using namespace std;
template <typename tupleType>
void DisplayTupleInfo (tupleType& tup)
{
const int numMembers = tuple_size<tupleType>::value; //tuple_size是什么意思?为什么有::, 为啥还有value?
cout << "Num elements in tuple: " << numMembers << endl;
cout << "Last element value: " << get<numMembers - 1>(tup) << endl;
// 这个get<numMembers - 1>(tup)是什么意思
}
int main()
{
tuple<int, char, string> tup1(make_tuple(101, 's', "Hello Tuple!"));
DisplayTupleInfo(tup1);
auto tup2(make_tuple(3.14, false));
DisplayTupleInfo(tup2);
auto concatTup(tuple_cat(tup2, tup1)); //contain tup2, tup1 members
DisplayTupleInfo(concatTup);
double pi;
string sentence;
tie(pi, ignore, ignore, ignore, sentence) = concatTup;
cout << "Unpacked! Pi: " << pi << " and \"" << sentence << "\"" << endl;
return 0;
}
这里先不作为重点学习,以后有需要的再看。(p416页 Analysis)
十三、使用static_assert执行编译检查
先放下这部分的概念。
十四、在实际的c++编程中使用模板
标准模板库(STL)是template的一个重要而强大的应用程序。STL由一组模板类和函数组成,其中包含通用实用程序类和算法。这些STL模板类使你能够实现动态数组、列表和键值对容器,而sort等算法则处理这些容器并处理它们包含的数据。
你在前面获得的模板语法知识极大地帮助您使用STL容器和函数,这些内容将在以后详细介绍。更好地理解STL容器和算法有助于您编写高效的c++应用程序,使用经过测试的STL和可靠的实现,并帮助您避免在样板细节上花费时间。
DO | DON'T |
---|---|
DO use templates for the implementation of generic concepts. | DON'T forget to use the principles of correctness when programming template functions and classes. |
Do choose templates over macros. | DON'T forget that a static member contained within a template class is static for every type-specialization of the class. |
十五、总结
每次运行编译器时,预处理器第一个开始运行并转换诸如#define之类的指令。预处理程序执行文本替换,尽管使用宏时可能有些复杂。宏函数根据编译时传递给宏的参数提供复杂的文本替换。在宏中把每个参数都加进括号内,以确保正确的替换是很重要的。
模板可以帮助你编写可重复使用的代码,为开发人员提供可用于各种数据类型的模式。他们还提供了一种类型安全的宏替换。