很多人觉得C++模板很难学习和适应,不值得浪费时间,今天它的白痴指南来了(第一部分)

文章背景

大多数C ++程序员由于其困惑的性质而远离C ++模板。 反对模板的借口:

  • 很难学习和适应。
  • 编译器错误是模糊的,而且很长。
  • 不值得的努力。

承认模板很难学习,理解和适应。 然而,我们从使用模板中获得的好处将超过负面影响。 有 比可以围绕模板包装的泛型函数或类要多得多。 我会说明他们。

从技术上讲,C ++模板和STL(标准模板库)是同级的。 在本文中,我只会介绍核心级别的模板。 本系列的下一部分将围绕模板介绍更高级和有趣的内容,以及有关STL的一些专门知识。

目录

语法句

  • 功能模板
  • 带有模板的指针,引用和数组
  • 带有功能模板的多种类型
  • 功能模板-模板功能
  • 显式模板参数规范
  • 功能模板的默认参数

类模板

  • 具有类模板的多种类型
  • 非类型模板参数
  • 模板类作为类模板的参数
  • 带类模板的默认模板参数
  • 类的方法作为功能模板

文末杂谈

【零声学院官方许可】2小时精通掌握《STL模板库》技术


语法句

您可能知道,模板很大程度上使用尖括号:小于( < )和大于( > )运算符。 对于模板,它们总是以这种形式一起使用:

< Content >

哪里可以用Content

  1. class T / typename T
  2. 数据类型,映射到 T
  3. 整体规格
  4. 映射到上述规范的整数常量/指针/参考。

对于点1和2,符号 T不过是某种数据类型,它可以是任何数据类型-基本数据类型( intdouble等)或UDT。

让我们跳到一个例子。 假设您编写了一个输出数字两倍(两倍)的函数:

void PrintTwice(int data)
{
    cout << "Twice is: " << data * 2 << endl;         
}

可以称为传递一个 int

PrintTwice(120); // 240

现在,如果要打印a的两倍 double,则可以将此函数重载为:

void PrintTwice(double data)
{
    cout << "Twice is: " << data * 2 << endl;         
}

有趣的是,类型 ostream (该 cout对象)具有用于多个重载 operator << -适用于所有基本数据类型。 因此,相同/相似的代码对 int和都适用 double,并且我们的 不需要更改 PrintTwice重载 -是的,我们只是 复制粘贴了 它。 如果我们使用 printf-functions之一,则这两个重载看起来像:

void PrintTwice(int data)
{
    printf("Twice is: %d", data * 2 );
}

void PrintTwice(double data)
{
    printf("Twice is: %lf", data * 2 );
}

这里的关键是不是 cout还是 print要在控制台上显示,但有关代码-这是 绝对相同的 。 这是 之一 我们可以利用C ++语言提供的常规功能的众多情况 :模板!

模板有两种类型:

  • 功能模板
  • 类模板

C ++模板是一种编程模型,它允许 将 插入 任何数据类型 到代码(模板代码)中。 没有模板,您将需要为所有必需的数据类型一次又一次地复制相同的代码。 显然,如前所述,它需要代码维护。

无论如何,这是 的 简化版 PrintTwice使用模板 :

void PrintTwice(TYPE data)
{
    cout<<"Twice: " << data * 2 << endl;
}

在此,实际 类型 TYPE将被推断通过根据传递给函数的参数的编译器(确定)。 如果 PrintTwice被称为 PrintTwice(144);这将是一个 int,如果你通过 3.14这个功能, TYPE就可以推断为 double类型。

您可能会感到困惑 TYPE,即编译器将如何确定这是一个函数模板。 是否在 TYPE使用 定义了类型 typedef某处 关键字 ?

不,我的孩子! 在这里,我们使用关键字 template让编译器知道我们正在定义函数模板。

功能模板

这是 模板 函数 PrintTwice

template<class TYPE>
void PrintTwice(TYPE data)
{
    cout<<"Twice: " << data * 2 << endl;
}

第一行代码:

template<class TYPE>

告诉编译器这是一个 功能模板。 的实际含义 TYPE将由编译器根据传递给此函数的参数推导出。 这里的名称 TYPE称为 模板类型形参

例如,如果我们将该函数称为:

PrintTwice(124);

TYPE将被编译器替换为 int,并且编译器 实例 将该模板函数 化为:

void PrintTwice(int data)
{
    cout<<"Twice: " << data * 2 << endl;
}

并且,如果我们将此函数称为:

PrintTwice(4.5547);

它将另一个实例化为:

void PrintTwice(double data)
{
    cout<<"Twice: " << data * 2 << endl;
}

这意味着,在您的程序中,如果 调用 ,则 PrintTwice使用 函数 intdouble参数类型 两个 编译器将生成此函数的 实例:

void PrintTwice(int data) { ... }
void PrintTwice(double data) { ... }

是的,代码是重复的。 但是这两个重载是由编译器而不是程序员实例化的。 真正的好处是您不必 也不必 也不必 复制粘贴 相同的代码, 为不同的数据类型手动维护代码, 为稍后出现的新数据类型编写新的重载。 您只需要提供 的 模板 函数 ,其余的将由编译器管理。

由于现在有两个函数定义,因此代码大小也会增加。 代码大小(在二进制/汇编级别)将几乎相同。 实际上,对于 N 个数据类型, N 将创建 个相同函数(即重载函数)的实例。 如果实例化的函数相同,或者函数主体的某些部分相同,则存在高级的编译器/链接器级别优化,可以在某种程度上减小代码大小。 我现在不讨论它。

但是,积极的一面是,当您手动定义 N个 不同的重载(例如 N=10)时, 这 N个 无论如何都将对 不同的重载进行编译,链接和打包为二进制文件(可执行文件)。 但是,使用模板, 只有 所需的函数实例化才能进入最终可执行文件。 使用模板,函数的重载副本可能少于N,并且可能超过N-但恰好是所需副本的数量-不少!

另外,对于非模板实现,编译器必须编译所有这N个副本-因为它们在您的源代码中! 当您 附加 模板 使用通用函数 时,编译器将仅针对所需的数据类型集进行编译。 这基本上意味着,如果不同数据类型的数量小于 则编译会更快 N,

这将是一个完全有效的论据,即编译器/链接器可能会进行所有可能的优化,以从最终映像中删除未使用的非模板函数的实现。 但是,再次,请理解编译器必须 编译 所有这些重载(用于语法检查等)。 使用模板,仅针对所需的数据类型进行编译-您可以将其称为“ 按需编译 ”。

现在只有纯文字内容! 您可以返回并再次阅读。 让我们继续前进。

现在,让我们编写另一个函数模板,该模板将返回给定数字的两倍:

template<typename TYPE>
TYPE Twice(TYPE data)
{
   return data * 2;
}

您应该已经注意到,我使用的是typeName,而不是class。不需要,如果函数返回某些内容,则不需要使用typeName关键字。对于模板编程,这两个关键字非常相似。有两个关键字用于同一目的是有历史原因的,我讨厌历史。

但是,在某些情况下,您只能使用较新的关键字-TypeName。(当特定类型在另一个类型中定义,并且依赖于某个模板参数时-让我们将此讨论推迟到另一个部分)。

继续前进。当我们将此函数调用为:

cout << Twice(10);
cout << Twice(3.14);
cout << Twice( Twice(55) );

将生成以下函数集:

int     Twice(int data) {..}
double  Twice(double data) {..}

在上面截取的第三行代码中,调用了两次-第一次调用的返回值/类型将是第二次调用的参数/类型。因此,这两个调用都是int类型(因为参数类型和返回类型是相同的)。
如果模板函数是针对特定数据类型实例化的,则编译器将重用相同函数的实例-如果针对相同数据类型再次调用该函数。这意味着,无论在代码中的何处,您都可以使用相同类型的函数模板来调用函数模板-在相同的函数中,在不同的函数中,或者在另一个源文件(相同的项目/构建)中的任何位置。

让我们编写一个返回两个数字相加的函数模板:

template<class T>
T Add(T n1, T n2)
{
    return n1 + n2;
}

首先,我只是将模板类型参数的name-type替换为符号T。在模板编程中,您通常会使用T-但这是个人选择。最好使用反映类型参数含义的名称,这样可以提高代码的可读性。此符号可以是遵循C++语言中变量命名规则的任何名称。

其次,我为两个参数(n1和n2)重用了模板参数T-。

让我们稍微修改一下Add函数,该函数将把加法结果存储在局部变量中,然后返回计算值。

template<class T>
T Add(T n1, T n2)
{
    T result;
    result = n1 + n2;
    
    return result;
}

很容易解释,我在函数体中使用了类型参数T。您可能会问(您应该):“当编译器试图编译/解析函数add时,它如何知道结果的类型?”

那么,当查看函数模板体(Add)时,编译器不会看到T(模板类型参数)是否正确。它只需检查基本语法(如分号、关键字的正确使用、匹配的大括号等),并报告这些基本检查的错误。同样,它依赖于编译器来编译它如何处理模板代码-但是它不会报告任何由于模板类型参数而导致的错误。

为了完整起见,我要重申,编译器不会检查(目前仅与函数添加相关):

  • T具有默认构造函数(因此 T result;有效)
  • T支持使用 operator + (这样才 <code>n1+n2有效)
  • T具有 可访问的 副本/移动构造函数(因此该 return语句成功)

本质上,编译器必须分两个阶段编译模板代码:一次进行基本语法检查; 稍后对 每个实例化 函数模板的 -它将对模板数据类型执行实际的代码编译。

如果您不完全理解这两个阶段的编译过程,那完全可以。 阅读本教程时,您将获得坚定的理解,然后稍后再阅读这些理论课程!

也许干巴巴的文字看起来有些枯燥,如果单看文字不是很容易消化的话,可以进群973961276来跟大家一起交流学习,群里也有许多视频资料和技术大牛,配合文章一起理解应该会让你有不错的收获。

推荐一个不错的c/c++ 初学者课程,这个跟以往所见到的只会空谈理论的有所不同,这个课程是从六个可以写在简历上的企业级项目入手带领大家学习c/c++,正在学习的朋友可以了解一下。

带有模板的指针,引用和数组

首先是一个代码示例(不用担心-这是简单的代码段!):

template<class T>
double GetAverage(T tArray[], int nElements)
{
    T tSum = T(); // tSum = 0

    for (int nIndex = 0; nIndex < nElements; ++nIndex)
    {
        tSum += tArray[nIndex];
    }

    // Whatever type of T is, convert to double
    return double(tSum) / nElements;
}
  

int main()
{
    int  IntArray[5] = {100, 200, 400, 500, 1000};
    float FloatArray[3] = { 1.55f, 5.44f, 12.36f};

    cout << GetAverage(IntArray, 5);
    cout << GetAverage(FloatArray, 3);
}

对于第一个电话 GetAverage,在那里 IntArray通过,编译器将实例化这个功能:

double GetAverage(int tArray[], int nElements);

和类似的 float。 类型,因此保留返回 double由于数字的平均值在逻辑上适合 double数据 类型。 请注意,这仅是本示例-所包含的实际数据类型 T可能是一个类,可能无法转换为 double

您应该注意,函数模板可能具有模板类型参数以及非模板类型参数。 它不需要具有功能模板的所有参数即可从模板类型到达。 int nElements是这样的函数参数。

显然,注意和理解,模板类型参数只是 T,而不是 T*T[] -编译器是足够聪明来推断类型 intint[](或 int*)。 在上面给出的示例中,我已将其用作 T tArray[]函数模板的参数,并且 实际数据类型 T可以从中智能地确定的 。

通常,您会碰到过,并且还需要使用初始化,例如:

T tSum = T();

首先,这不是模板特定的代码-它属于C ++语言本身。 从本质上讲,这意味着:调用 的 默认构造函数 此数据类型 。 对于 int,它将是:

int tSum = int();

有效地使用初始化变量 0。 同样,对于 float,它将将此变量设置为 0.0f。 尽管尚未涵盖,但是如果用户定义的类类型来自 T,它将调用该类的默认构造函数(如果可调用,否则相关的错误)。 如您所知,它 T可能是任何数据类型,我们不能 初始化 tSum简单地使用整数零( 0)进行 。 实际上,它可能是某个字符串类,它使用空字符串( 对其进行初始化 "") 。

由于模板类型 T可以是任何类型,因此它也必须 += operator可用。 正如我们所知,它是可用于所有的基本数据类型( intfloatchar等)。 如果实际类型(用于 T)没有 +=可用的运算符(或任何可能性),则编译器将引发一个错误,即实际类型不具有该运算符,或任何可能的转换。

同样,类型 T必须能够将其自身转换为 double(请参见以下 return语句)。 稍后,我将掩盖这些棘手的问题。 为了更好地理解,我 重新列出了所需的 支持 从类型中 T(现在仅适用于 GetAverage功能模板):

  • 必须具有 可访问的 默认构造函数。
  • 必须具有 += operator可通话性。
  • 必须能够将自身转换为 double(或等效值)。

对于 GetAverage 功能模板原型,可以使用 T*代替 T[],并且含义相同:

template<class T>
GetAverage(T* tArray, int nElements){}

由于调用方将传递一个数组(分配在堆栈或堆上)或类型为的变量的地址 T。 但是,您应该知道,这些规则属于C ++的规则集,而并非专门来自模板编程!

前进。 让我们问 演员 参考 ”来为模板编程 轻弹 。 现在,不言而喻,您只是将其 T& 用作基础类型的函数模板参数 T

template<class T>
void TwiceIt(T& tData)
{
    tData *= 2;    
    // tData = tData + tData;
}

它计算参数的两倍值,并将其放入相同参数的值中。 您可以简单地称呼它为:

int x = 40;
TwiceIt(x); // Result comes as 80

请注意,我过去常常 operator *=两次争论 tData。 您也可以使用 operator + 以获得相同的效果。 对于基本数据类型,两个运算符均可用。 对于类类型,不是两个运算符都可用,您可能会要求该类实现必需的运算符。

我认为, 是合乎逻辑的 operator +按班级定义 。 原因很简单- 这样做 T+T对于大多数UDT(用户定义类型)而言, 更合适 *= operator。 问问自己:如果某些类 这意味着什么 StringDate实现或被要求实现以下运算符, :

void operator *= (int); // void return type is for simplicity only.

在这一点上,你现在清醒地认识到模板参数类型 T可以推断 T&T*T[] 因此, 也是可行且非常 合理的 将 添加 const属性 到要到达功能模板的参数 ,并且该参数不会被功能模板更改。 放轻松,它很简单:

template<class TYPE>
void PrintTwice(const TYPE& data)
{
    cout<<"Twice: " << data * 2 << endl;
}

观察到我已经将模板参数修改 TYPETYPE&,并且也添加 const了它。 很少或大多数读者会意识到这种变化的重要性。 对于那些没有的人:

  • TYPE类型的大小可能很大,并且将需要更多的堆栈空间(调用堆栈)。 它包括 double需要8个字节 *,某些结构或类的类,这将需要更多字节保留在堆栈上。 从本质上讲,这意味着-将创建给定类型的新对象,调用复制构造函数,并将其放入调用堆栈中,然后在函数结尾处进行析构函数调用。
    引用( &)的添加避免了所有这些情况- 引用 传递同一对象的 。
  • 函数不会更改传递的参数,因此 const不会对其进行添加。 对于函数的调用者,它确保此函数(此处为 PrintTwice)不会更改参数的值。 如果函数本身错误地尝试修改( 内容,则还可以确保发生编译器错误 constant )参数的

在32位平台上,函数参数至少需要4个字节,并且至少需要4个字节。 这意味着一个 charshort将在调用堆栈中需要4个字节。 例如,一个11字节的对象需要12字节的堆栈。
同样,对于64位平台,将需要8个字节。 一个11字节的对象将需要16个字节。 类型的参数 double将需要8个字节。
在32位/ 64位平台上,所有指针/引用分别占用4字节/ 8字节,因此 位平台,传递 doubledouble&对于64 意味着相同。

同样,我们应该将其他功能模板更改为:

template<class TYPE>
TYPE Twice(const TYPE& data) // No change for return type
{
   return data * 2;
}

template<class T>
T Add(const T& n1, const T& n2) // No return type change
{
    return n1 + n2;
}

template<class T>
GetAverage(const T tArray[], int nElements)
// GetAverage(const T* tArray, int nElements)
{}

注意 不可能有引用并将其 const,除非我们打算返回传递给函数模板的原始对象的引用(或指针),否则 添加到返回类型。 以下代码举例说明了它:

template<class T>
T& GetMax(T& t1, T& t2)
{
    if (t1 > t2)
    {
        return t2;
    }
    // else 
    return t2;
}

这就是我们利用返回引用的方式:

int x = 50;
int y = 64;

// Set the max value to zero (0)
GetMax(x,y) = 0;

请注意,这只是出于说明目的,您很少会看到或编写此类代码。 但是,如果返回的对象是某个UDT的引用,则可能会看到这样的代码并且可能需要编写。 在这种情况下,成员访问运算符点( .)或箭头( ->)将跟随函数调用。 无论如何,此函数模板返回 的 引用 赢得大于竞赛的对象 。 当然,这需要 operator >按type定义 T

您应该已经注意到,我尚未添加 const任何两个传递的参数。 这是必需的; 由于函数返回类型的非常量引用 T。 曾经是这样的:

T& GetMax(const T& t1, const T& t2)

在这些 return语句中,编译器会抱怨 t1t2无法将其转换为非常量。 如果我们 添加 const还将返回类型也 为( const T& GetMax(...) ),则调用网站上的以下行将无法编译:

GetMax(x,y) = 0;

由于 const对象无法修改! 您绝对可以在函数中或在调用站点中进行强制const /非const类型转换。 但这是一个不同的方面,一个糟糕的设计和一个不推荐的方法。

带有功能模板的多种类型

到目前为止,我只介绍了一种类型作为模板类型参数。 使用模板,您可能有多个模板类型参数。 就像这样:

template<class T1, class T2, ... >

其中 T1T2是功能模板的类型名称。 您可以使用任何其他特定的名称,而不是 T1T2。 需要注意的是的“使用 ...”上面并 没有 意味着这个模板规范可以采取任何数量的参数。 仅说明模板可以具有任意数量的参数。
(与C ++ 11标准一样,模板将允许可变数量的参数-但目前为止,这已经超出了主题。)

让我们看一个使用两个模板参数的简单示例:

template<class T1, class T2>
void PrintNumbers(const T1& t1Data, const T2& t2Data)
{
     cout << "First value:" << t1Data;
     cout << "Second value:" << t2Data;
}

我们可以简单地称其为:

PrintNumbers(10, 100);    // int, int
PrintNumbers(14, 14.5);   // int, double
PrintNumbers(59.66, 150); // double, int

每个调用都需要为传递的第一种和第二种类型(或说是 单独的模板实例化 推断的 )使用 。 因此,编译器将填充以下三个功能模板实例:

// const and reference removed for simplicity
void PrintNumbers(int t1Data, int t2Data);
void PrintNumbers(int t1Data, double t2Data);
void PrintNumbers(double t1Data, int t2Data);

认识到,第二和第三实例是不一样的, T1并且 T2将推断不同数据类型( intdoubledoubleint)。 编译器将 不会 执行任何自动转换,就像正常函数调用可能会执行的那样- 一个采用的正常函数 int例如,可以传递 , short反之亦然。 但是对于模板,如果您通过 short-它是绝对的 short,不是(升级为) int。 因此,如果您传递( shortint),( shortshort),( longint)-这将导致 三个不同的实例化 PrintNumbers!的 。

以类似的方式,函数模板可以具有3个或更多类型参数,并且它们每个都将映射到函数调用中指定的参数类型。 例如,以下功能模板是合法的:

template<class T1, class T2, class T3>
T2 DoSomething(const T1 tArray[], T2 tDefaultValue, T3& tResult)
{
   ... 
}

Where T1指定调用者将传递的数组类型。 如果未传递数组(或指针),则编译器将呈现适当的错误。 该类型 T2用作返回类型以及通过值传递的第二个参数。 类型 T3作为引用(非常量引用)传递。 上面给出的此功能模板示例只是随意选择的,但它是有效的功能模板规范。


到目前为止,我已经详细介绍了多个模板参数。但出于某种原因,我现在开始使用一个参数函数。这是有原因的,你很快就会明白的。

假设有一个函数( 模板化),它带有一个 int参数:

void Show(int nData);

您将其称为:

Show( 120 );    // 1
Show( 'X' );    // 2
Show( 55.64 );  // 3
  • 调用 1 完全有效,因为函数接受 int参数,而我们正在传递 120
  • 调用 2 是有效的调用,因为我们正在传递 char,编译器会将其提升为 int
  • 调用 3 将要求降级值-编译器必须转换 doubleint,因此 55将传递而不是 55.64。 是的,这将触发适当的编译器警告。

一种解决方案是修改函数,使其采用 double,可以传递所有三种类型。 但这并不支持所有类型,并且可能不适合或转换为 double。 因此,您可以使用适当的类型编写一组重载函数。 有了知识,现在,您将了解模板的重要性,并要求将其编写为功能模板:

template<class Type>
void Show(Type tData) {}

当然,假设所有现有的重载 Show都在做相同的事情。

好吧,你知道这个练习。 那么,导致我 辞职的新消息是 什么?

好吧,如果您想传递 int给函数模板 Show,但希望编译器像 一样实例化 double传递通过 呢?

// This will produce (instantiate) 'Show(int)'
Show ( 1234 );

// But you want it to produce 'Show(double)'

截至目前,要求这件事似乎不合逻辑。 但是有充分的理由要求这种实例化,您很快就会理解和赞赏!

无论如何,首先要了解如何要求这样荒唐的事情:

Show<double> ( 1234 );

实例化以下 模板函数 (如您所知):

void Show(double);

使用这种特殊的语法( Show<>()),您要求编译器为 实例化 Show显式传递的类型 函数,并要求编译器 不要 按函数参数推断类型。

功能模板-模板功能

重要! 之间有区别 函数模板 模板函数

一个 函数模板 是括号周围的函数体 template的关键字,这是不实际的功能,并不会 完全 由编译器编译,而不是通过链接的责任。 至少需要一个针对特定数据类型的调用来实例化它,并将其纳入编译器和链接器的职责范围。 因此,功能模板 Show的实例被实例化为 Show(int)Show(double)

一个 模板函数 ? 简而言之,就是一个“函数模板的实例”,它是在您调用它时生成的,或者使它针对特定的数据类型实例化。 函数模板的实例实际上是有效的函数。

在编译器和链接器的名称装饰系统的保护下,功能模板的一个实例(又称为模板功能)不是普通功能。 这意味着函数模板的一个实例:

template<class T> 
void Show(T data) 
{ }

对于模板参数 double,它 不是

void Show(double data){}

但实际上:

void Show<double>(double x){}

长期以来,我只是为了简单而未发现这个问题,现在您知道了! 使用编译器/调试器找出函数模板的实际实例,并在调用堆栈或生成的代码中查看函数的完整原型。

因此,现在您知道了这两者之间的映射:

Show<double>(1234);
...
void Show<double>(double data); // Note that data=1234.00, in this case!

显式模板参数规范

退一步(向上)到多模板参数讨论。

我们有以下功能模板:

template<class T1, class T2>
void PrintNumbers(const T1& t1Data, const T2& t2Data)
{}

并具有以下函数调用,导致此函数模板的3个不同实例:

PrintNumbers(10, 100);    // int, int
PrintNumbers(14, 14.5);   // int, double
PrintNumbers(59.66, 150); // double, int

而且,如果您只需要一个实例-两个参数都取用 double怎么办? 是的,您愿意通过 int并让他们晋升 double。 加上您刚刚获得的理解,您可以将此函数模板称为:

PrintNumbers<double, double>(10, 100);    // int, int
PrintNumbers<double, double>(14, 14.5);   // int, double
PrintNumbers<double, double>(59.66, 150); // double, int

这只会产生以下 模板函数

void PrintNumbers<double, double>(const double& t1Data, const T2& t2Data)
{}

从呼叫站点以这种方式传递模板类型参数的概念被称为“ 显式模板参数规范”。

为什么需要显式类型说明? 好吧,有多种原因:

  • 您只希望传递特定类型,而不希望编译器 智能地 推断 仅通过实际参数(函数参数) 一种或多种模板参数类型。

例如,有一个函数模板, max带有 两个 参数(仅通过 一个 模板类型参数):

template<class T>
T max(T t1, T t2)
{
   if (t1 > t2)
      return t1;
   return t2;
}

您尝试将其称为:

max(120, 14.55);

这将导致编译器错误,并指出template-type含糊不清 T。 您要让编译器从两种类型中推断出一种类型! 一种解决方案是更改 max模板,使其具有两个模板参数-但您不是该功能模板的作者。

在那里使用显式参数规范:

max<double>(120, 14.55); // Instantiates max<double>(double,double);

毫无疑问地注意到并理解,我仅对 传递了明确的规范 第一个 模板参数 ,第二个类型是从函数调用的第二个参数推导出的。

  • 当function-template采用template-type时,而不是从其function参数中获取时。

一个简单的例子:

template<class T>
void PrintSize()
{
   cout << "Size of this type:" << sizeof(T);
}

您不能简单地调用以下函数模板:

PrintSize();

由于此函数模板将需要模板类型参数规范,因此编译器无法自动推导该模板。 正确的调用是:

PrintSize<float>();

将 实例化 PrintSize使用 float模板参数 。

  • 当函数模板具有不能从参数推导出的返回类型时,或者当函数模板没有任何参数时。

一个例子:

template<class T>
T SumOfNumbers(int a, int b)
{
   T t = T(); // Call default CTOR for T

   t = T(a)+b;

 
   return t;
}

这需要两个 ints并将其求和。 尽管将它们 求和 int本身 是合适的,但是此函数模板提供了 机会 来计算 的和(使用 operator+调用者要求的任何类型 )。 例如,在中获取结果 double,您可以将其称为:

double nSum;
nSum = SumOfNumbers<double>(120,200);

最后两个只是为了完整起见而简化的示例,只是为了 您 提示 适合使用“显式模板参数规范”。在更具体的场景中, 这种 显式性 需要 ,并将在下一部分中进行介绍。

功能模板的默认参数

对于 读者而言, 确实 了解模板领域中默认模板类型规范的 这 无关 与默认模板类型参数 。 无论如何,默认模板类型是功能模板所不允许的。 对于读者来说,谁也 不会 知道这件事情,不用担心-这一段是不是默认的模板类型规范。

如您所知,C ++函数可能具有默认参数。 defaultness只能从右到左,这意味着,如果 ,则 第n个 第 要求 参数为默认值 ( n + 1 也必须为默认值,依此类推直到函数的最后一个参数。

一个简单的例子来说明这一点:

template<class T>
void PrintNumbers(T array[], int array_size, T filter = T())
{
   for(int nIndex = 0; nIndex < array_size; ++nIndex)
   {
       if ( array[nIndex] != filter) // Print if not filtered
           cout << array[nIndex];
   }
}

您可能会猜到,此函数模板将打印所有数字,除了被第三个参数过滤掉的数字 filter。 最后一个可选的函数参数默认为type的default-value T,对于所有基本类型均表示为零。 因此,当您将其称为:

int Array[10] = {1,2,0,3,4,2,5,6,0,7};
PrintNumbers(Array, 10);

它将被实例化为:

void PrintNumbers(int array[], int array_size, int filter = int())
{}

filter参数将呈现为: int filter = 0

很明显,当您将其称为:

PrintNumbers(Array, 10, 2);

第三个参数获取值 2,而不是默认值 0

应该清楚地了解:

  • 类型 T必须具有可用的默认构造函数。 当然,函数主体可能会要求type的所有运算符 T
  • 默认参数必须 可以 推导出来 从模板采用的其他非默认类型中 。 在 PrintNumbers例子中,类型 array将有助于扣除 filter
    如果不是,则必须使用显式模板参数规范来指定默认参数的类型。

可以肯定的是,默认参数不一定是类型的默认值 T(请原谅)。 这意味着,默认参数可能并不总是需要依赖于类型的default-constructor T

template<class T>
void PrintNumbers(T array[], int array_size, T filter = T(60))

在这里,默认函数参数不为type使用default-value T。 相反,它使用value 60。 当然,这要求该类型 T具有可接受 copy-constructor int(for 60)的 。

最后,本文这一部分的“功能模板”到此结束。 我认为您喜欢阅读和掌握这些 基础知识 功能模板的 。 下一部分将涵盖模板编程的更多有趣方面。

类模板

通常,您将设计和使用类模板而不是功能模板。 通常,您使用类模板来定义一种抽象类型,该抽象类型的行为是通用的并且可重用,适应性强。 虽然有些文本将从给出有关数据结构的示例开始,例如链表,堆栈,队列和类似的 容器 。 我将从非常简单的非常简单的示例开始。

让我们看一个简单的类,该类设置,获取和打印存储的值:

class Item
{
    int Data;
public:
    Item() : Data(0)
    {}

    void SetData(int nValue)
    { 
        Data = nValue;
    }

    int GetData() const
    {
        return Data;
    }

    void PrintData()
    {
        cout << Data;
    }
};

一个初始化 构造函数 Data0,Set和Get方法的 ,以及一个用于打印当前值的方法。 用法也很简单:

Item item1;
item1.SetData(120);
item1.PrintData(); // Shows 120

当然,没有什么适合您的! 但是,当您需要对其他数据类型进行类似的抽象时,则需要复制整个类的代码(或至少复制所需的方法)。 它引起代码维护问题,增加源代码和二进制级别的代码大小。

是的,我能感觉到我将要提到C ++模板的情报! 形式的同一类的模板化版本 类模板 如下:

template<class T>
class Item
{
    T Data;
public:
    Item() : Data( T() )
    {}

    void SetData(T nValue)
    {
        Data = nValue;
    }

    T GetData() const
    {
        return Data;
    }

    void PrintData()
    {
        cout << Data;
    }
};

类模板声明以与函数模板相同的语法开头:

template<class T>
class Item

请注意,该关键字 class使用了两次-首先用于指定模板类型规范( T),其次用于指定这是C ++类声明。

要完全转 Item成类模板,我更换的所有实例 intT。 我还使用 T()语法 调用的默认构造函数 T,而不是硬编码 0在构造函数的初始值设定项列表中 (零)。 如果您已 阅读 功能模板 完整 部分,则知道原因!

而且用法也很简单:

Item<int> item1;
item1.SetData(120);
item1.PrintData();

与函数模板实例化不同,函数模板的参数本身会帮助编译器推断模板类型的参数,而使用类模板,则必须显式传递模板类型(在尖括号中)。

上面显示的代码片段使类模板 Item实例化为 Item<int>。 当使用 创建具有不同类型的另一个对象时 Item类模板 :

Item<float> item2;
float n = item2.GetData();

这将导致 Item<float>实例化。 重要的是要知道,类模板- 两个实例之间绝对没有关系 Item<int>和的 Item<float>。 对于编译器和链接器,这两个是不同的实体,或者说是不同的类。

使用type的第一个实例 int产生以下方法:

  • Item<int>::Item()建设者
  • SetDataPrintData类型的方法 int

类似地,类型第二次实例化 float将产生:

  • Item<float>::Item()建设者
  • GetData方法 float类型的

如您所知 Item<int>Item<float>是两种不同的类/类型; 因此,以下代码将不起作用:

item1 = item2; // ERROR : Item<float> to Item<int>

由于两种类型不同,因此编译器将不会调用 可能的 默认赋值运算符。 如果 item1item2具有相同的类型(都使用 Item<int>),则编译器会很高兴地调用赋值运算符。 尽管对于编译器来说,可以在 之间 intfloat 进行转换,但是即使基础数据成员相同,也不可能进行不同的UDT转换-这是简单的C ++规则。

在这一点上,清楚地了解到只有以下方法集会被实例化:

  • Item<int>::Item()-构造函数
  • void Item<int>::SetData(int)方法
  • void Item<int>::PrintData() const方法
  • Item<float>::Item()-构造函数
  • float Item<float>::GetData() const方法

以下方法将 无法 进行第二阶段编译:

  • int Item<int>::GetData() const
  • void Item<float>::SetData(float)
  • void Item<float>::PrintData() const

现在,什么是第二阶段编译? 好了,正如我已经阐述的那样,无论是否调用/实例化模板代码,都将对其进行编译以进行基本语法检查。 这称为第一阶段编译。

当您实际调用或以某种方式触发它调用特定类型的函数/方法时-只有它才能得到 特殊待遇 第二阶段 编译的 。 只有通过第二阶段的编译,代码才实际针对实例化的类型进行完全编译。

虽然,我本可以早点详细说明,但是这个地方合适。 您如何确定函数是否正在进行第一阶段和/或第二阶段编译?

让我们做一些奇怪的事情:

T GetData() const
 { 
  for())

  return Data;
 }

末尾有一个括号, for这是不正确的。 编译它时, 它,都会收到很多错误 无论 是否调用 。 我已经使用Visual C ++和GCC编译器对其进行了检查,并且都抱怨。 这验证了第一阶段的编译。

让我们将其稍微更改为:

T GetData() const
{ 
  T temp = Data[0]; // Index access ?
  return Data;
}

现在,在 编译 调用 GetData为任何类型 方法的情况下进行 -编译器将不会产生任何终止作用。 这意味着,此功能目前尚未得到第二阶段的编译处理!

致电后:

Item<double> item3;
item2.GetData();

您会从 编译器中得到错误,而该错误 Data不是数组或指针的 可能已 数组或指针 operartor []附加到 上。 证明只有选择的函数才能获得第二阶段编译的特殊特权。 对于实例化类/函数模板的所有唯一类型,此第二阶段编译将分别进行。

您可以做的一件有趣的事情是:

T GetData() const
{ 
  return Data % 10;
}

可以成功为编译 Item<int>,但失败 Item<float>

item1.GetData(); // item1 is Item<int>

// ERROR
item2.GetData(); // item2 is Item<float>

由于 operator %不适用于 float类型。 有趣吗?

具有类模板的多种类型

我们的第一类模板 Item只有一个模板类型。 现在,让我们构造一个具有两个模板类型参数的类。 同样,可能会有一些复杂的类模板示例,我想保持简单。

有时,您确实需要一些本机结构来保留少量数据成员。 一件商品制作独特商品 struct为同 似乎有些不必要和不必要的工作。 您很快就会从名称很少的不同结构中脱颖而出。 另外,它增加了代码长度。 无论您对此有何看法,我都以它为例,并派生一个包含两个成员的类模板。

STL程序员会发现这等同于 std::pair类模板。

假设您有一个结构 Point

struct Point
{
    int x;
    int y;
};

其中有两个数据成员。 此外,您可能还具有其他结构 Money

struct Money
{
    int Dollars;
    int Cents;
};

这两种结构都具有几乎相似的数据成员。 与其重写不同的结构,不如将它放在一个地方会更好,这也将有助于:

  • 具有一个或两个给定类型的参数的构造函数,以及一个复制构造函数。
  • 比较两个相同类型对象的方法。
  • 在两种类型之间交换
  • 和更多。

您可能会说可以使用继承模型,在该模型中定义所有必需的方法,然后让派生类对其进行自定义。 它适合吗? 您选择的数据类型呢? 它可能是 intstringfloat一些类 的类型 简而言之,继承只会使设计复杂化,而不会允许C ++模板促进插件功能。

在那里,我们使用类模板! 只需 定义 的类 模板即可 为两种类型 具有所有必需方法 。 开始吧!

template<class Type1, class Type2>
struct Pair
{
    // In public area, since we want the client to use them directly.
    Type1 first;
    Type2 second;
};

现在,我们可以使用 Pair类模板来 派生 具有两个成员的任何类型。 一个例子:

// Assume as Point struct
Pair<int,int> point1;

// Logically same as X and Y members
point1.first = 10;
point1.second = 20;

了解 类型 firstsecond现在 是 int和的 int分别 。 这是因为我们 实例化 Pair用这些类型 了。

当我们实例化它时:

Pair<int, double> SqRoot;

SqRoot.first = 90;
SqRoot.second = 9.4868329;

first将是 int类型,并且 second将是 double类型。 清楚地了解 firstsecond是数据成员,而不是函数,因此 不会对运行时造成任何损失 假定的 函数调用 。

注意 :在本文的此部分,所有定义仅在类声明主体内。 在下一部分中,我将解释如何在单独的实现文件中实现方法以及与此相关的问题。 因此,所示的所有方法定义都应仅假设在此范围内 class ClassName{...};

下面给出的默认构造函数初始化会成员都为它们的默认值,按数据类型 Type1Type2

Pair() : first(Type1()), second(Type2())
{}

以下是带参数的构造函数,采用 Type1Type2初始化 值 firstand的 second

Pair(const Type1& t1, const Type2& t2) : 
  first(t1), second(t2)
  {}

以下是一个复制构造函数,它将 复制一个 Pair从另一个 对象 Pair完全相同类型的对象 :

Pair(const Pair<Type1, Type2>& OtherPair) : 
  first(OtherPair.first),
  second(OtherPair.second)
 {}

请注意,非常需要 指定的模板类型参数 Pair<>为此复制构造函数的参数 。 下列规范没有意义,因为 Pair不是 非模板类型:

Pair(const Pair& OtherPair) // ERROR: Pair requires template-types

这是一个使用参数化构造函数和copy-constructor的示例:

Pair<int,int> point1(12,40);
Pair<int,int> point2(point1);

重要的是要注意,如果您更改任何一个对象 任何模板类型参数 point2或的 point1 则将无法使用 复制构造它 point1object 。 以下将是一个错误:

Pair<int,float> point2(point1);  // ERROR: Different types, no conversion possible.

虽然,之间有可能转换 floatint,但之间没有可能转换 Pair<int,float>Pair<int,int>。 复制构造函数不能将其他 类型 用作可复制对象。 有一个解决方案,但是我将在下一部分中讨论它。

您可以类似的方式实现比较运算符,以比较两个相同 对象 Pair类型的 。 以下是等于操作符的实现:

bool operator == (const Pair<Type1, Type2>& Other) const
{
  return first == Other.first && 
         second == Other.second;
}

请注意,我使用 const属性,参数和方法本身。 请充分理解上述方法定义的第一行!

就像copy-constructor调用一样,您必须将完全相同的类型传递给此比较运算符-编译器不会尝试转换不同的 Pair类型。 一个例子:

if (point1 == point2) // Both objects must be of same type.
 
   ...

为了对此处介绍的概念有扎实的理解,请自行实现以下方法:

  • 其余所有5个关系运算符
  • 赋值运算符
  • Swap方法
  • 修改两个构造函数(copy-constructor除外),并将它们组合为一个,以便它们将两个参数都用作默认值。 这意味着,仅实现一个可以接受0,1或2个参数的构造函数。

Pairclass是两种类型的示例,可以使用它代替定义仅具有两个数据成员的多个结构。 缺点是只是记住什么 firstsecond意味着(X或Y?)。 但是,当您很好地定义模板实例化时,您始终会 了解和使用 first及其 second适当地 成员。

忽略这一缺点,您将实现 中的所有功能 实例化 类型 :构造函数,复制构造函数,比较运算符,交换方法等。而且,无需重新编写各种两元结构所需的代码,您将获得所有这些。您将需要。 此外,如您所知,只有一组 必需的 方法会被编译和链接。 类模板中的错误修复将自动反映到所有实例中。 是的,如果修改不符合现有用法,则对类模板进行的轻微修改也可能会引发其他类型的错误。

同样,您可以具有一个类模板 模板 tuple,该 允许三个(或更多)数据成员。 请尽量实现类 tuple具有三个成员( firstsecondthird)自己:

template<class T1, class T2, class T3>
class tuple

非类型模板参数

好了,我们已经看到类模板和函数模板一样,可以采用多个类型参数。 但是类模板也允许很少的非类型模板参数。 在这一部分中,我将仅阐述一个非类型: integer

是的,类模板可以采用整数作为模板参数。 首先是一个样本:

template<class T, int SIZE>
class Array{};

在此类模板声明中, int SIZE是一个非类型参数,它是一个整数。

  • 只有积分数据类型可以是非整数型参数,它包括 intcharlonglong longunsigned变体和 enum第 诸如 这样的类型 floatdouble不允许使用 。
  • 实例化时,只能传递编译时常数整数。 这意味着 100100+991<<3等是允许的,因为它们被编译时间常数表达式。 包含涉及函数调用的参数,例如 abs(-120)不允许 。
    作为模板参数,如果可以将浮点数/双精度数等转换为整数,则可以允许它们。

精细。 我们可以实例化类模板 Array为:

Array<int, 10> my_array;

所以呢? 的目的是 SIZE争论 什么?

好吧,在类模板中,可以在任何可能使用整数的地方使用此非类型整数参数。 这包括:

  • 分配类的静态const数据成员。
template<class T, int SIZE>
class Array
{
 static const int Elements_2x = SIZE * 2; 
};

[类声明的前两行将不再显示,假定所有内容都在类的主体之内。]

由于允许 初始化 static-constant-integer 在类声明中 ,因此我们可以使用非类型的整数参数。

  • 指定方法的默认值。
    (尽管,C ++还允许将任何非常量作为函数的默认参数,我已经指出了这一点只是为了说明。)
void DoSomething(int arg = SIZE); 
// Non-const can also appear as default-argument...
  • 定义数组的大小。

这一点很重要,非类型整数参数通常用于此目的。 因此,让我们 实现类模板 Array利用 SIZE参数来 。

private:
   T TheArray[SIZE];

T是数组的类型, SIZE是大小(整数)-就这么简单。 由于数组位于类的私有区域中,因此我们需要定义几个方法/运算符。

// Initialize with default (i.e. 0 for int)
void Initialize()
{ 
    for(int nIndex = 0; nIndex < SIZE; ++nIndex)
        TheArray[nIndex] = T();
}

当然,类型 T必须具有默认构造函数和赋值运算符。 我将介绍 这些内容( 要求 在下一部分中, 功能模板和类模板的 )。

我们还需要实现数组元素访问运算符。 一个重载的索引访问运算符集,另一个获得值(类型 T):

T operator[](int nIndex) const
{
  if (nIndex>0 && nIndex<SIZE)
  {
    return TheArray[nIndex];
  }
  return T();
}

T& operator[](int nIndex)
{
   return TheArray[nIndex];
}

请注意,第一个重载(声明为 const)是get / read方法,并检查索引是否有效,否则返回类型的默认值 T.

第二次重载返回 的 引用 元素 ,调用者可以对其进行修改。 没有索引有效性检查,因为它必须返回引用,因此 local-object( T()无法返回 )。 但是,您可以检查index参数,返回默认值,使用 断言 和/或引发异常。

让我们定义另一个方法,该方法将在 逻辑上 求和 Array

T Accumulate() const
{
  T sum = T();
  for(int nIndex = 0; nIndex < SIZE; ++nIndex)
  {
     sum += TheArray[nIndex];
  }
  return sum;
}

如您所解释的,它要求 operator +=可用于target type T。 还要注意,返回类型 T本身就是合适的。 因此,当 实例化 Array用某个字符串类 时,它将 时调用 +=在每次迭代 并返回组合的字符串。 如果目标类型没有 此 +=定义 运算符,而您调用此方法,则将出现错误。 在这种情况下,您要么-不要打电话; 或在目标类中实现所需的运算符重载。

模板类作为类模板的参数

尽管这是一个模糊的陈述,并引起一些歧义,但我会尽力消除模糊感。

首先,回顾一下 之间的区别 template-function function-template 。 如果 神经元 已经帮助将正确的信息传递到 的 缓存 大脑 中,那么您现在可以 回调 template-function是function-template的一个实例。 如果您的大脑搜索子系统没有响应,请 重新加载信息

一个实例 类模板的 模板类。 因此,对于以下类模板:

template<class T1, class T2>
class Pair{};

该模板的实例是一个模板类:

Pair<int,int> IntPair;

清醒地认识到 IntPair不是 一个模板类,是 不是 对于类模板实例。 它是一个 对象 的特定实例/类模板的。 模板类/实例是 Pair<int,int>,它产生了另一个类类型(编译器,我们的朋友做到了,您知道!)。 本质上,这是在这种情况下编译器将生成的模板类:

class Pair<int,int>{};

模板类有更精确的定义,请选择以下单行代码以便于理解。 详细的说明将在本系列的下一部分中进行。

现在,让我们说清楚。 如果您将模板类传递给某个类模板怎么办? 我的意思是,以下陈述意味着什么?

Pair<int, Pair<int,int> > PairOfPair;

是否有效-如果是这样,这是什么意思?
首先,它是完全有效的。 其次,它实例化了 两个 模板类:

  • Pair<int,int>- 一个
  • Pair<int, Pair<int,int> >-

无论 一个 类型会被编译器进行实例化,如果有任何错误,因为任何类型的这两个模板类的产生,编译器将报告。 为了简化这种 复杂的 实例,您可以执行以下操作:

typedef Pair<int,int> IntIntPair;
...
Pair<int, IntIntPair> PairOfPair;

您可以这样分配 firstsecond成员 PairOfPair对象的 :

PairOfPair.first = 10;
PairOfPair.second.first = 10;
PairOfPair.second.second= 30;

请注意, second最后两行中的member是type Pair<int,int>,因此它具有相同的一组成员以供进一步访问。 这就是原因 firstsecond成员都可以使用,以 级联 的方式。

现在,您(希望)了解到类模板( Pair)将模板类( Pair<int,int> 作为参数并引入了最终实例化!

在此讨论中,一个有趣的实例将 Array与一起使用 Pair! 您知道 Pair有两个模板类型参数, Array一个类型参数和一个大小(整数)参数。

Array< Pair<int, double>, 40> ArrayOfPair;

这是 intdouble的类型参数 Pair。 因此, 的第一个模板类型 Array(标记为粗体) 为 Pair<int,double>。 第二个参数是常量 40。 您能回答这个问题吗:的构造函数会 Pair<int,double>被调用吗? 什么时候会被调用? 在您回答之前,我只是将实例反转为:

Pair<int, Array<double, 50>> PairOfArray;

哇! 这是什么意思?
好吧,这意味着: PairOfArray是的实例化 Pair,将第一种类型作为 int (对于 first成员),而第二种类型( second)是一个 Array。 其中 Array(的第二种类型 type的 Pair)是 50元素 double

不要为此而杀了我! 慢慢并清楚地了解模板的这些基本概念。 一旦获得了清晰的理解,您就会 喜欢 模板!

再一次,我使用了模板类( Array<double,50>)作为其他类型( 实例的参数 Pair<int,...>) 。

好的,但是 的右移运算符( >>上面 )在做什么? 嗯,这不是运算符,而只是 Array类型说明的结尾,然后是 的结尾 Pair类型说明 。 一些旧的编译器要求我们在两个大于号之间插入一个空格,以避免出现错误或混乱。

Pair<int, Array<double, 50>  > PairOfArray;

当前,几乎所有现代C ++编译器都足够聪明,足以了解它用于结束模板类型规范,因此您不必担心。 因此,您可以随意使用两个或多个 >符号来结束模板规范。

注意,在C ++术语中,传递模板类(实例化)并不是很具体-它只是类模板所采用的一种类型。

最后,在这里我将用法示例放到两个对象中。 首先是构造函数。

Array< Pair<int, double>, 40> ArrayOfPair;

这将导致的构造函数 Pair被调用 40 次,因为在 声明了常量大小的数组 Array类模板中 :

T TheArray[SIZE];

这将意味着:

Pair<int,double> TheArray[40];

因此,需要调用数量的构造函数 Pair

对于以下对象构造:

Pair<int, Array<double, 50>> PairOfArray;

构造 Pair将与初始化第一个参数 0(使用 int()符号),并且将呼叫的构造 ArrayArray()符号,如下图所示:

Pair() : first(int()), second(Array())
 {}

由于 的默认构造函数 Array类模板 由编译器提供,因此它将被调用。 如果您不理解此处编写的内容,请提高您的C ++技能。

分配的一个元素 ArrayOfPair

ArrayOfPair[0] = Pair<int,double>(40, 3.14159);

在这里,您正在调用的非常量版本 版本 Array::operator[],该 将返回 的 引用 第一个元素 Array (from TheArray) 。 如您所知,该元素是type Pair<int,double>。 赋值运算符右侧的表达式只是调用构造函数, Pair<int,double>并传递所需的两个参数。 分配完成!

带类模板的默认模板参数

首先,让我消除与“默认参数”短语的任何歧义。 “功能模板”部分中使用了相同的短语。 在该小节中,默认参数是指功能参数本身的参数,而不是功能模板的类型参数。 无论如何,函数模板 支持模板参数的默认参数。 附带说明一下,请知道类模板的方法可以采用默认参数,就像任何普通函数/方法都可以采用那样。

另一方面,类模板确实为模板参数的type / non-type参数支持default-argument。 举个例子:

template<class T, int SIZE=100>
class Array
{
private:
   T TheArray[SIZE];
   ...
};

我刚刚 了修改 SIZE在类模板的第一行中进行 Array。 第二个模板参数,即整数常量规范,现在设置为 100。 这意味着,当您以以下方式使用它时:

Array<int> IntArray;

从本质上讲,这意味着:

Array<int, 100> IntArray;

在实例化此类模板期间,编译器会自动将其放置。 当然,您可以通过显式传递第二个模板参数来指定自定义数组的大小:

Array<int, 200> IntArray;

请记住,当您通过类模板声明中指定的相同参数显式传递默认参数的参数时,它将仅实例化一次。 我的意思是,创建的以下两个对象将仅实例化一个类: Array<int,100>

Array<int> Array1;
Array<int,100> Array2;

当然,如果您在类模板定义中更改默认的template参数(值为以外的值) 100,则会导致两个模板实例化,因为它们是不同的类型。

您可以使用 自定义默认参数 const#define

const int _size = 120;
// #define _size 150 
template<class T, int SIZE=_size>
class Array

当然,使用 _size符号代替硬编码的常数表示相同。 但是使用符号会简化默认的'规范。 无论您如何为整数指定默认模板参数(这是一个非类型模板参数),它都必须是一个编译时间常数表达式。

你一般会 使用默认规范非类型整型参数,除非你正在使用的模板,先进的东西,如元编程,静态断言,SFINAE等,这肯定需要一个单独的部分。 更常见的是,您会看到并实现类模板的默认参数,即 数据类型 。 一个例子:

template<class T = int>
class Array100
{
    T TheArray[100];
};
 

它定义了一个 类型的数组 Tsize 100。 在这里,type参数默认为 int。 这意味着,如果您在实例化时未指定类型 Array100,则它将映射到 int。 以下是有关如何使用它的示例:

Array100<float> FloatArray;
Array100<> IntArray;

在第一个实例中,我 传递 float以模板类型 ,而在第二个调用中,我 将其保留为默认值( int使用 ) <>符号 。 尽管此符号在模板编程中有更多用途,我将在后面的部分中进行介绍,但这种情况也非常需要。 如果您尝试将类模板用作:

Array100 IntArray;

这将导致编译器错误,即 Array100需要模板参数。 因此, 必须使用尖括号( 空集 <>如果所有模板参数均为默认值,并且您希望使用默认值,则 )的 实例化类模板。

要记住重要的事情是一个非模板类的名字 Array100不会 被也是允许的。 如下所示,非模板类的定义以及模板类(彼此之间或之上或之下)的定义将使编译器不满意:

class Array100{}; // Array100 demands template arguments!

现在,让我们在类中混合使用type和non-type参数 Array

template<class T = int, int SIZE=100>
class Array
{
    T TheArray[SIZE];
    ...
};

最后,type和size参数分别用 标记为default int100。 清楚地了解到,第一个 int用于的默认规范 T,第二个 int用于非模板常量规范。 为了简化和提高可读性,应将它们放在不同的行中:

template<class T = int, 
         int SIZE=100>
class Array{};

现在,使用您的智能来解析以下实例化的含义:

Array<>            IntArray1;
Array<int>         IntArray2;
Array<float, 40>   FlaotArray3;

就像 一样 函数模板中的显式说明 ,不允许仅指定尾随模板参数。 以下是错误:

Array<, 400> IntArrayOf500; // ERROR

最后,请记住,在创建两个对象之后将仅实例化一个类模板,因为它们实际上是完全相同的:

Array<>          IntArray1;
Array<int>       IntArray2
Array<int, 100>  IntArray3;

将模板类型默认为其他类型

也可以在先前到达的模板参数上默认设置type / non-type参数。 例如 ,我们可以修改 Pair,如果未明确指定第二种类型 该类,以使第二种类型与第一种类型相同。

template<class Type1, class Type2 = Type1>
class Pair
{
    Type1 first;
    Type2 second;
};

在此修改后的类模板中 PairType2现在默认为 Type1type。 实例化的例子:

Pair<int> IntPair;

您可以猜测,它与:

Pair<int,int> IntPair;

但是,您不必输入第二个参数。 也可以将第一个参数 Pair设为default:

template<class Type1=int, class Type2 = Type1>
class Pair
{
    Type1 first;
    Type2 second;
};

这意味着,如果你没有通过任何模板参数, Type1int,因此 Type2 也将是 int

用法如下:

Pair<> IntPair;

实例化以下类:

class Pair<int,int>{};

当然,也可以在另一个非类型参数上默认非类型参数。 一个例子:

template<class T, int ROWS = 8, int COLUMNS = ROWS>
class Matrix
{
    T TheMatrix[ROWS][COLUMNS];
};

但是, 从属 模板参数必须在 的 右边 其所依赖 。 以下将导致错误:

template<class Type1=Type2, class Type2 = int>
class Pair{};

template<class T, int ROWS = COLUMNS, int COLUMNS = 8>
class Matrix

类的方法作为功能模板

虽然,这不是绝对的初学者,但是由于我同时介绍了函数模板和类模板,因此对该概念的阐述对于本系列文章的第一部分而言是合乎逻辑的。

考虑一个简单的例子:

class IntArray
{
    int TheArray[10];
public:
    template<typename T>
    void Copy(T target_array[10])
    {
       for(int nIndex = 0; nIndex<10; ++nIndex)
       {
          target_array[nIndex] = TheArray[nIndex];
          // Better approach: 
            //target_array[nIndex] = static_cast<T>(TheArray[nIndex]);
       }
    }
};

该类 IntArray是简单的非模板类,具有 的整数数组 10元素 。 但是该方法 Copy被设计为功能模板(方法模板?)。 它采用一个模板类型参数,该参数将由编译器自动推导。 这是我们如何使用它:

IntArray int_array;
float float_array[10];

int_array.Copy(float_array);

您可能猜到了, IntArray::Copy将使用type实例化 float,因为我们将float数组传递给了它。 为了避免混淆,并更好地理解它,只是觉得 int_array.Copy作为 Copy唯一的,并 IntArray::Copy<float>(..)作为 Copy<float>(..)唯一的。 类的 方法 模板不过是嵌入在类中的普通功能模板。

请注意,我 使用 10到处都 数组大小。 有趣的是,我们还可以将类修改为

template<int ARRAY_SIZE>
class IntArray
{
    int TheArray[ARRAY_SIZE];
public:
    template<typename T>
    void Copy(T target_array[ARRAY_SIZE])
    {
       for(int nIndex = 0; nIndex<ARRAY_SIZE; ++nIndex)
       {
            target_array[nIndex] = static_cast<T>(TheArray[nIndex]);
       }
    }
};

使得类 IntArray和方法 Copy,更好的候选人在模板编程领域!

就像您已经聪明地猜到的那样, Copy方法只不过是一个数组转换例程,该例程可以从转换 int为任何类型,只要 转换为 int有可能就可以 给定类型。 这是一种有效的情况,其中类方法可以作为函数模板编写,可以自己获取模板参数。 请修改此类 模板 ,以使其可用于任何类型的数组,而不仅限于 int.

当然,带有方法模板的“显式模板参数指定”也是可能的。 考虑另一个示例:

template<class T>
class Convert
{   
   T data;
public: 
   Convert(const T& tData = T()) : data(tData)
   { }

   template<class C>   
   bool IsEqualTo( const C& other ) const      
   {        
       return data == other;   
   }
};

可以用作:

Convert<int> Data;
float Data2 = 1 ;

bool b = Data.IsEqualTo(Data2);

实例化 Convert::IsEqualTofloat参数 。 如下所示,显式规范将使用实例化它 double

bool b = Data.IsEqualTo<double>(Data2);

令人惊讶的事情之一是,借助模板,您可以通过在模板之上定义转换运算符来做到这一点!

template<class T>
operator T() const
{
    return data;
} 

Convert只要有可能,就可以 '类模板实例转换为任何类型。 考虑以下用法示例:

Convert<int> IntData(40);
float FloatData;
double DoubleData;

 
FloatData = IntData;
DoubleData = IntData;

它将实例化以下两种方法(完全限定的名称):

Convert<int>::operator<float> float();
Convert<int>::operator<double> double();

一方面,它提供了良好的灵活性,因为无需编写额外的代码, Convert就可以将自身(特定的实例化)转换为任何数据类型-只要在编译级别可以进行转换即可。 如果无法进行转换,例如从 转换为 doubleto 字符串类型,则会引发错误。

但是,另一方面,它也可能由于无意中插入错误而引起麻烦。 您可能不希望调用转换运算符,并且在您不了解转换运算符的情况下调用了该转换运算符(生成了编译器代码)。

在最后

您刚刚看到模板所提供的强大功能和灵活性。 下一部分将介绍更多高级和有趣的概念。 我对所有的读者谦逊和有抱负的要求是 发挥 与模板越来越多。 尝试首先在一个方面获得牢固的了解(仅像功能模板一样),而不是匆忙跳到其他概念。 最初是使用您的 测试 项目/代码库,而不是任何现有的/正在运行的/生产的代码。

以下是我们所涵盖内容的摘要:

  • 为了避免不必要的代码重复和代码维护问题,特别是当代码完全相同时,我们可以使用模板。 模板比使用在空指针之上运行的C / C ++宏或函数/类要好得多。
  • 模板不仅是类型安全的,而且还减少了不会被引用(不是由编译器生成)的不必要的代码膨胀。
  • 函数模板用于放置不属于类的代码,并且对于不同的数据类型,该代码相同/几乎相同。 在大多数地方,编译器会自动确定类型。 否则,您必须指定类型,也可以自己指定显式类型。
  • 类模板使围绕特定实现包装任何数据类型成为可能。 它可以是数组,字符串,队列,链表,线程安全的 原子 实现等。类模板确实有助于默认模板类型规范,而功能模板不支持该规范。

希望您喜欢这篇文章,并清除了使模板变得复杂(不必要地是奇怪)的思想障碍。 第二部分将很快到达!

猜你喜欢

转载自blog.csdn.net/linuxguitu/article/details/112525654