实现OpenGL渲染器语法篇(四)——template模板的使用

最近在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 

// A template function
bool TemplateFunction (const T1 ¶m1, const T2 & param2);

// A template class
template 
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 
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  (num1, num2);

// sample usage two:
double double1 = 1.1;
double double2 = 1.001;

double maxVal = GetMax  (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后面去掉了

在这种case下,编译器是非常聪明的,可以明白这个模板函数是为整数类型调用的。

但是,对于模板类而言,大家需要显式地指明type!!

// A template function GetMax that helps evaluate the higher of two supplied values

#include 
#include 

using namespace std;

// 到模板的声明和一系列定义了

template 
const Type& GetMax (const Type& value1, const Type& value2)
{
    if (value1 > value2)
        return value1;
    else
        return value2;
}

template 
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  (num1, num2);

大家只需要记住一点:

  • 编写模板函数时,可以不显式。但是编写模板类的时候,是需要显式的。

五、模板和类型安全

第四章的栗子中的模板函数都是type safe的。这意味着这两个模板函数不允许有毫无意义的调用:

DisplayComparison (num1, name1);

如果写成这样,将会立即导致编译失败。(num1和name1是不同数据类型的)


六、模板类

类是封装某些属性,和可以对这些属性进行操作的方法的编程单元。

属性通常是私有成员,例如中的。(年龄就是人的一种属性)

类可以被看作是一个宏伟的设计蓝图,类的实际表现形式是类的对象

  • 为什么要使用模板类呢?

例如,Tom可以被看作是的一个对象,它的属性年龄的值是15。如果出于应用程序的特定原因,需要你将年龄存储为自出生以来的秒数,如果是针对这种情况,将数据定义为型应该是不够的。为了程序的安全考虑,大家可能会想到用来代替。

那么碰到这个情况的时候,模板类就要派上用场了。

模板类是c++类的模板化版本。也可以说是设计蓝图的设计蓝图

  • 优点:当你在使用模板类时,你是可以指定要专门化该类的类型的。

这个优点可以让你,创造一些人的classes,这些class的模板参数可以是,一些使用,一些使用的整数。

一个简单的模板类,它可以使用一个单一的参数,来保存一个成员变量。具体实现如下:

template 
class HoldVarTypeT
{
    private:
    T value;
    
    public:
    void SetValue (const T& newValue)
    {
        value = newValue;
    }
    
    T& GetValue()
    {
        return value;
    }
}

上面这个示例中,变量的类型是,这个类型是在模板被使用的时候,也就是被实例化的时候给变量分配的。这个成员变量,就是存储在参数中的。

那么我们往后学,大家看一下下面这个模板类的具体应用:

HoldVarTypeT  holdInt;     //  int类型的模板实例化
holdInt.SetValue(5);
cout << "The value stored is: " << holdInt.GetValue() << endl;

就是一个模板类,它指定为一个int类型的成员变量。
也就是说,现在大家已经可以使用这个模板类保存和检索int类型的对象

也就是说,模板类是为int类型的模板参数实例化的。类似地,大家也可以使用相同的类以类似的方式处理字符串。

HoldVarTypeT  holdStr;  //  这个是什么鬼?
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 holdHuman;
holdHuman.SetValue(firstMan);
holdHuman.GetValue().IntroduceSelf();  //这行没太看懂。。。

上面的第一行代码就是说,HoldVarTypeT是一个模板类,Human是一个class,class作为template的参数,来定义变量holdHuman。(也就是说,class也可以作为template的参数,从而来实例化类Human的模板!)


七、声明具有多个参数的模板template

  • 如何声明多个参数?

可以展开模板参数列表来声明由逗号分隔的多个参数。

因此,如果你希望声明一个泛型类,该类包含一对可以是不同类型的对象,你可以使用以下示例中所示的结构来实现这一点(显示一个带有两个模板参数的模板类):

template 
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  pairIntDouble (6, 1.99);

//  A template instantiation that pairs an int with an int
//  一个模板实例化,它可以将一个int型与一个int型配对的模板实例化
HoldsPair  pairIntDouble (6, 500);
//  这个pairIntDouble是什么啊??

上面这个示例中,第一行中,T1指定类型为int,T2指定类型为double。
第二行中,T1和T2的类型均是int类型。


八、使用默认参数声明模板

大家可以修改之前版本的将int型声明为默认模板参数类型

template 
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 
using namespace std;

// template with default params: int & double
// 具有默认参数的模板:int和double
template 
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 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  pairIntDb1;

那么,就相当于,你指示编译器使用模板为你创建了一个类,并将其实例化为指定的模板参数的类型(在本例中为)。

因此,

对于模板而言,实例化是使用一个或多个模板参数创建一个特定类型的行为或过程。


另一方面,在使用特定类型实例化时,可能会出现需要让你显式定义一个(不同)的模板的行为的情况。

这是你专门化一个类型的模板(或其行为)的地方。

当使用两个模板参数进行实例化时,模板类的特例化是下面这样:

template <> class HoldsPair 
{
    // implementation code here
}

// template和class写在了一行
// class后面加上了

显而易见的是,专门处理模板的代码必须遵循模板定义。

下面给大家看个栗子,这个栗子的核心是一个模板的特例化,演示了一个专门化版本和一个模板特例化后有多大的区别。(an example of a template specialization that demonstrates how different a specialized version can be from the template it specializes.)

// Demonstrates Template Specialization
#include 
using namespace std;

template 
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 
{
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  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 
using namespace std;

template 
class TestStatic
{
public:
    static int staticVal;
};

//  static member initialization
template  int TestStatic::staticVal;  //这句没看懂啊

int main()
{
    TestStatic intInstance;
    cout << "Setting staticVal for intInstance to 2011" << endl;
    intInstance.staticVal = 2011;
    
    TestStatic 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  int TestStatic::staticVal;

// This follows the pattern

template