大多数C ++程序员由于其困惑的性质而远离C ++模板。 反对模板的借口:
承认模板很难学习,理解和适应。 然而,我们从使用模板中获得的好处将超过负面影响。 有 比可以围绕模板包装的泛型函数或类要多得多。 我会说明他们。
从技术上讲,C ++模板和STL(标准模板库)是同级的。 在本文中,我只会介绍核心级别的模板。 本系列的下一部分将围绕模板介绍更高级和有趣的内容,以及有关STL的一些专门知识。
【零声学院官方许可】2小时精通掌握《STL模板库》技术
您可能知道,模板很大程度上使用尖括号:小于( <
)和大于( >
)运算符。 对于模板,它们总是以这种形式一起使用:
< Content >
哪里可以用Content
:
class T
/ typename T
T
对于点1和2,符号 T
不过是某种数据类型,它可以是任何数据类型-基本数据类型( int
, double
等)或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
void PrintTwice(TYPE data)
{
cout<<"Twice: " << data * 2 << endl;
}
第一行代码:
template
告诉编译器这是一个 功能模板。 的实际含义 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
使用 函数 int
和 double
参数类型 两个 编译器将生成此函数的 实例:
void PrintTwice(int data) { ... }
void PrintTwice(double data) { ... }
是的,代码是重复的。 但是这两个重载是由编译器而不是程序员实例化的。 真正的好处是您不必 也不必 也不必 复制粘贴 相同的代码, 为不同的数据类型手动维护代码, 为稍后出现的新数据类型编写新的重载。 您只需要提供 的 模板 函数 ,其余的将由编译器管理。
由于现在有两个函数定义,因此代码大小也会增加。 代码大小(在二进制/汇编级别)将几乎相同。 实际上,对于 N 个数据类型, N 将创建 个相同函数(即重载函数)的实例。 如果实例化的函数相同,或者函数主体的某些部分相同,则存在高级的编译器/链接器级别优化,可以在某种程度上减小代码大小。 我现在不讨论它。
但是,积极的一面是,当您手动定义 N个 不同的重载(例如 N=10
)时, 这 N个 无论如何都将对 不同的重载进行编译,链接和打包为二进制文件(可执行文件)。 但是,使用模板, 只有 所需的函数实例化才能进入最终可执行文件。 使用模板,函数的重载副本可能少于N,并且可能超过N-但恰好是所需副本的数量-不少!
另外,对于非模板实现,编译器必须编译所有这N个副本-因为它们在您的源代码中! 当您 附加 模板 使用通用函数 时,编译器将仅针对所需的数据类型集进行编译。 这基本上意味着,如果不同数据类型的数量小于 则编译会更快 N, 。
这将是一个完全有效的论据,即编译器/链接器可能会进行所有可能的优化,以从最终映像中删除未使用的非模板函数的实现。 但是,再次,请理解编译器必须 编译 所有这些重载(用于语法检查等)。 使用模板,仅针对所需的数据类型进行编译-您可以将其称为“ 按需编译 ”。
现在只有纯文字内容! 您可以返回并再次阅读。 让我们继续前进。
现在,让我们编写另一个函数模板,该模板将返回给定数字的两倍:
template
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
T Add(T n1, T n2)
{
return n1 + n2;
}
首先,我只是将模板类型参数的name-type替换为符号T。在模板编程中,您通常会使用T-但这是个人选择。最好使用反映类型参数含义的名称,这样可以提高代码的可读性。此符号可以是遵循C++语言中变量命名规则的任何名称。
其次,我为两个参数(n1和n2)重用了模板参数T-。
让我们稍微修改一下Add函数,该函数将把加法结果存储在局部变量中,然后返回计算值。
template
T Add(T n1, T n2)
{
T result;
result = n1 + n2;
return result;
}
很容易解释,我在函数体中使用了类型参数T。您可能会问(您应该):“当编译器试图编译/解析函数add时,它如何知道结果的类型?”
那么,当查看函数模板体(Add)时,编译器不会看到T(模板类型参数)是否正确。它只需检查基本语法(如分号、关键字的正确使用、匹配的大括号等),并报告这些基本检查的错误。同样,它依赖于编译器来编译它如何处理模板代码-但是它不会报告任何由于模板类型参数而导致的错误。
为了完整起见,我要重申,编译器不会检查(目前仅与函数添加相关):
T
具有默认构造函数(因此 T result;
有效)T
支持使用 operator +
(这样才 n1+n2
有效)
T
具有 可访问的 副本/移动构造函数(因此该 return
语句成功)本质上,编译器必须分两个阶段编译模板代码:一次进行基本语法检查; 稍后对 每个实例化 函数模板的 -它将对模板数据类型执行实际的代码编译。
如果您不完全理解这两个阶段的编译过程,那完全可以。 阅读本教程时,您将获得坚定的理解,然后稍后再阅读这些理论课程!
也许干巴巴的文字看起来有些枯燥,如果单看文字不是很容易消化的话,可以进群973961276来跟大家一起交流学习,群里也有许多视频资料和技术大牛,配合文章一起理解应该会让你有不错的收获。
推荐一个不错的c/c++ 初学者课程,这个跟以往所见到的只会空谈理论的有所不同,这个课程是从六个可以写在简历上的企业级项目入手带领大家学习c/c++,正在学习的朋友可以了解一下。
首先是一个代码示例(不用担心-这是简单的代码段!):
template
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[]
-编译器是足够聪明来推断类型 int
从 int[]
(或 int*
)。 在上面给出的示例中,我已将其用作 T tArray[]
函数模板的参数,并且 实际数据类型 T
可以从中智能地确定的 。
通常,您会碰到过,并且还需要使用初始化,例如:
T tSum = T();
首先,这不是模板特定的代码-它属于C ++语言本身。 从本质上讲,这意味着:调用 的 默认构造函数 此数据类型 。 对于 int
,它将是:
int tSum = int();
有效地使用初始化变量 0
。 同样,对于 float
,它将将此变量设置为 0.0f
。 尽管尚未涵盖,但是如果用户定义的类类型来自 T
,它将调用该类的默认构造函数(如果可调用,否则相关的错误)。 如您所知,它 T
可能是任何数据类型,我们不能 初始化 tSum
简单地使用整数零( 0
)进行 。 实际上,它可能是某个字符串类,它使用空字符串( 对其进行初始化 ""
) 。
由于模板类型 T
可以是任何类型,因此它也必须 += operator
可用。 正如我们所知,它是可用于所有的基本数据类型( int
, float
, char
等)。 如果实际类型(用于 T
)没有 +=
可用的运算符(或任何可能性),则编译器将引发一个错误,即实际类型不具有该运算符,或任何可能的转换。
同样,类型 T
必须能够将其自身转换为 double
(请参见以下 return
语句)。 稍后,我将掩盖这些棘手的问题。 为了更好地理解,我 重新列出了所需的 支持 从类型中 T
(现在仅适用于 GetAverage
功能模板):
+= operator
可通话性。double
(或等效值)。对于 GetAverage
功能模板原型,可以使用 T*
代替 T[]
,并且含义相同:
template
GetAverage(T* tArray, int nElements){}
由于调用方将传递一个数组(分配在堆栈或堆上)或类型为的变量的地址 T
。 但是,您应该知道,这些规则属于C ++的规则集,而并非专门来自模板编程!
前进。 让我们问 演员 “ 参考 ”来为模板编程 轻弹 。 现在,不言而喻,您只是将其 T&
用作基础类型的函数模板参数 T
:
template
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
。 问问自己:如果某些类 这意味着什么 String
或 Date
实现或被要求实现以下运算符, :
void operator *= (int); // void return type is for simplicity only.
在这一点上,你现在清醒地认识到模板参数类型 T
可以推断 T&
, T*
或 T[]
。
因此, 也是可行且非常 合理的 将 添加 const
属性 到要到达功能模板的参数 ,并且该参数不会被功能模板更改。 放轻松,它很简单:
template
void PrintTwice(const TYPE& data)
{
cout<<"Twice: " << data * 2 << endl;
}
观察到我已经将模板参数修改 TYPE
为 TYPE&
,并且也添加 const
了它。 很少或大多数读者会意识到这种变化的重要性。 对于那些没有的人:
TYPE
类型的大小可能很大,并且将需要更多的堆栈空间(调用堆栈)。 它包括 double
需要8个字节 *
,某些结构或类的类,这将需要更多字节保留在堆栈上。 从本质上讲,这意味着-将创建给定类型的新对象,调用复制构造函数,并将其放入调用堆栈中,然后在函数结尾处进行析构函数调用。&
)的添加避免了所有这些情况- 引用 传递同一对象的 。const
不会对其进行添加。 对于函数的调用者,它确保此函数(此处为 PrintTwice
)不会更改参数的值。 如果函数本身错误地尝试修改( 内容,则还可以确保发生编译器错误 constant )参数的在32位平台上,函数参数至少需要4个字节,并且至少需要4个字节。 这意味着一个
char
或short
将在调用堆栈中需要4个字节。 例如,一个11字节的对象需要12字节的堆栈。
同样,对于64位平台,将需要8个字节。 一个11字节的对象将需要16个字节。 类型的参数double
将需要8个字节。
在32位/ 64位平台上,所有指针/引用分别占用4字节/ 8字节,因此 位平台,传递double
或double&
对于64 意味着相同。
同样,我们应该将其他功能模板更改为:
template
TYPE Twice(const TYPE& data) // No change for return type
{
return data * 2;
}
template
T Add(const T& n1, const T& n2) // No return type change
{
return n1 + n2;
}
template
GetAverage(const T tArray[], int nElements)
// GetAverage(const T* tArray, int nElements)
{}
注意 不可能有引用并将其 const
,除非我们打算返回传递给函数模板的原始对象的引用(或指针),否则 添加到返回类型。 以下代码举例说明了它:
template
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
语句中,编译器会抱怨 t1
或 t2
无法将其转换为非常量。 如果我们 添加 const
还将返回类型也 为( const T& GetMax(...)
),则调用网站上的以下行将无法编译:
GetMax(x,y) = 0;
由于 const
对象无法修改! 您绝对可以在函数中或在调用站点中进行强制const /非const类型转换。 但这是一个不同的方面,一个糟糕的设计和一个不推荐的方法。
到目前为止,我只介绍了一种类型作为模板类型参数。 使用模板,您可能有多个模板类型参数。 就像这样:
template
其中 T1
和 T2
是功能模板的类型名称。 您可以使用任何其他特定的名称,而不是 T1
, T2
。 需要注意的是的“使用 ...
”上面并 没有 意味着这个模板规范可以采取任何数量的参数。 仅说明模板可以具有任意数量的参数。
(与C ++ 11标准一样,模板将允许可变数量的参数-但目前为止,这已经超出了主题。)
让我们看一个使用两个模板参数的简单示例:
template
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
将推断不同数据类型( int
, double
和 double
, int
)。 编译器将 不会 执行任何自动转换,就像正常函数调用可能会执行的那样- 一个采用的正常函数 int
例如,可以传递 , short
反之亦然。 但是对于模板,如果您通过 short
-它是绝对的 short
,不是(升级为) int
。 因此,如果您传递( short
, int
),( short
, short
),( long
, int
)-这将导致 三个不同的实例化 PrintNumbers
!的 。
以类似的方式,函数模板可以具有3个或更多类型参数,并且它们每个都将映射到函数调用中指定的参数类型。 例如,以下功能模板是合法的:
template
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
int
参数,而我们正在传递 120
。char
,编译器会将其提升为 int
。double
为 int
,因此 55
将传递而不是 55.64
。 是的,这将触发适当的编译器警告。一种解决方案是修改函数,使其采用 double
,可以传递所有三种类型。 但这并不支持所有类型,并且可能不适合或转换为 double
。 因此,您可以使用适当的类型编写一组重载函数。 有了知识,现在,您将了解模板的重要性,并要求将其编写为功能模板:
template
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 ( 1234 );
实例化以下 模板函数 (如您所知):
void Show(double);
使用这种特殊的语法( Show<>()
),您要求编译器为 实例化 Show
显式传递的类型 函数,并要求编译器 不要 按函数参数推断类型。
重要! 之间有区别 函数模板 和 模板函数 。
一个 函数模板 是括号周围的函数体 template
的关键字,这是不实际的功能,并不会 完全 由编译器编译,而不是通过链接的责任。 至少需要一个针对特定数据类型的调用来实例化它,并将其纳入编译器和链接器的职责范围。 因此,功能模板 Show
的实例被实例化为 Show(int)
或 Show(double)
。
一个 模板函数 ? 简而言之,就是一个“函数模板的实例”,它是在您调用它时生成的,或者使它针对特定的数据类型实例化。 函数模板的实例实际上是有效的函数。
在编译器和链接器的名称装饰系统的保护下,功能模板的一个实例(又称为模板功能)不是普通功能。 这意味着函数模板的一个实例:
template
void Show(T data)
{ }
对于模板参数 double
,它 不是 :
void Show(double data){}
但实际上:
void Show(double x){}
长期以来,我只是为了简单而未发现这个问题,现在您知道了! 使用编译器/调试器找出函数模板的实际实例,并在调用堆栈或生成的代码中查看函数的完整原型。
因此,现在您知道了这两者之间的映射:
Show(1234);
...
void Show(double data); // Note that data=1234.00, in this case!
退一步(向上)到多模板参数讨论。
我们有以下功能模板:
template
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(10, 100); // int, int
PrintNumbers(14, 14.5); // int, double
PrintNumbers(59.66, 150); // double, int
这只会产生以下 模板函数:
void PrintNumbers(const double& t1Data, const T2& t2Data)
{}
从呼叫站点以这种方式传递模板类型参数的概念被称为“ 显式模板参数规范”。
为什么需要显式类型说明? 好吧,有多种原因:
例如,有一个函数模板, max
带有 两个 参数(仅通过 一个 模板类型参数):
template
T max(T t1, T t2)
{
if (t1 > t2)
return t1;
return t2;
}
您尝试将其称为:
max(120, 14.55);
这将导致编译器错误,并指出template-type含糊不清 T
。 您要让编译器从两种类型中推断出一种类型! 一种解决方案是更改 max
模板,使其具有两个模板参数-但您不是该功能模板的作者。
在那里使用显式参数规范:
max(120, 14.55); // Instantiates max(double,double);
毫无疑问地注意到并理解,我仅对 传递了明确的规范 第一个 模板参数 ,第二个类型是从函数调用的第二个参数推导出的。
一个简单的例子:
template
void PrintSize()
{
cout << "Size of this type:" << sizeof(T);
}
您不能简单地调用以下函数模板:
PrintSize();
由于此函数模板将需要模板类型参数规范,因此编译器无法自动推导该模板。 正确的调用是:
PrintSize();
将 实例化 PrintSize
使用 float
模板参数 。
一个例子:
template
T SumOfNumbers(int a, int b)
{
T t = T(); // Call default CTOR for T
t = T(a)+b;
return t;
}
这需要两个 int
s并将其求和。 尽管将它们 求和 int
本身 是合适的,但是此函数模板提供了 机会 来计算 的和(使用 operator+
调用者要求的任何类型 )。 例如,在中获取结果 double
,您可以将其称为:
double nSum;
nSum = SumOfNumbers(120,200);
最后两个只是为了完整起见而简化的示例,只是为了 您 提示 适合使用“显式模板参数规范”。在更具体的场景中, 这种 显式性 需要 ,并将在下一部分中进行介绍。
对于 读者而言, 确实 了解模板领域中默认模板类型规范的 这 无关 与默认模板类型参数 。 无论如何,默认模板类型是功能模板所不允许的。 对于读者来说,谁也 不会 知道这件事情,不用担心-这一段是不是默认的模板类型规范。
如您所知,C ++函数可能具有默认参数。 defaultness只能从右到左,这意味着,如果 ,则 第n个 第 要求 参数为默认值 ( n + 1 ) 个 也必须为默认值,依此类推直到函数的最后一个参数。
一个简单的例子来说明这一点:
template
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
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;
}
};
一个初始化 构造函数 Data
为 0
,Set和Get方法的 ,以及一个用于打印当前值的方法。 用法也很简单:
Item item1;
item1.SetData(120);
item1.PrintData(); // Shows 120
当然,没有什么适合您的! 但是,当您需要对其他数据类型进行类似的抽象时,则需要复制整个类的代码(或至少复制所需的方法)。 它引起代码维护问题,增加源代码和二进制级别的代码大小。
是的,我能感觉到我将要提到C ++模板的情报! 形式的同一类的模板化版本 类模板 如下:
template
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 Item
请注意,该关键字 class
使用了两次-首先用于指定模板类型规范( T
),其次用于指定这是C ++类声明。
要完全转 Item
成类模板,我更换的所有实例 int
用 T
。 我还使用 T()
语法 调用的默认构造函数 T
,而不是硬编码 0
在构造函数的初始值设定项列表中 (零)。 如果您已 阅读 功能模板 完整 部分,则知道原因!
而且用法也很简单:
Item item1;
item1.SetData(120);
item1.PrintData();
与函数模板实例化不同,函数模板的参数本身会帮助编译器推断模板类型的参数,而使用类模板,则必须显式传递模板类型(在尖括号中)。
上面显示的代码片段使类模板 Item
实例化为 Item
。 当使用 创建具有不同类型的另一个对象时 Item
类模板 :
Item item2;
float n = item2.GetData();
这将导致 Item
实例化。 重要的是要知道,类模板- 两个实例之间绝对没有关系 Item
和的 Item
。 对于编译器和链接器,这两个是不同的实体,或者说是不同的类。
使用type的第一个实例 int
产生以下方法:
Item::Item()
建设者SetData
和 PrintData
类型的方法 int
类似地,类型第二次实例化 float
将产生:
Item::Item()
建设者GetData
方法 float
类型的如您所知 Item
, Item
是两种不同的类/类型; 因此,以下代码将不起作用:
item1 = item2; // ERROR : Item to Item
由于两种类型不同,因此编译器将不会调用 可能的 默认赋值运算符。 如果 item1
和 item2
具有相同的类型(都使用 Item
),则编译器会很高兴地调用赋值运算符。 尽管对于编译器来说,可以在 之间 int
和 float
进行转换,但是即使基础数据成员相同,也不可能进行不同的UDT转换-这是简单的C ++规则。
在这一点上,清楚地了解到只有以下方法集会被实例化:
Item<int>::Item()
-构造函数void Item::SetData(int)
方法void Item::PrintData() const
方法Item<float>::Item()
-构造函数float Item::GetData() const
方法以下方法将 无法 进行第二阶段编译:
int Item::GetData() const
void Item::SetData(float)
void Item::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 item3;
item2.GetData();
您会从 编译器中得到错误,而该错误 Data
不是数组或指针的 可能已 数组或指针 operartor []
附加到 上。 证明只有选择的函数才能获得第二阶段编译的特殊特权。 对于实例化类/函数模板的所有唯一类型,此第二阶段编译将分别进行。
您可以做的一件有趣的事情是:
T GetData() const
{
return Data % 10;
}
可以成功为编译 Item
,但失败 Item
:
item1.GetData(); // item1 is Item
// ERROR
item2.GetData(); // item2 is Item
由于 operator %
不适用于 float
类型。 有趣吗?
我们的第一类模板 Item
只有一个模板类型。 现在,让我们构造一个具有两个模板类型参数的类。 同样,可能会有一些复杂的类模板示例,我想保持简单。
有时,您确实需要一些本机结构来保留少量数据成员。 一件商品制作独特商品 struct
为同 似乎有些不必要和不必要的工作。 您很快就会从名称很少的不同结构中脱颖而出。 另外,它增加了代码长度。 无论您对此有何看法,我都以它为例,并派生一个包含两个成员的类模板。
STL程序员会发现这等同于 std::pair
类模板。
假设您有一个结构 Point
,
struct Point
{
int x;
int y;
};
其中有两个数据成员。 此外,您可能还具有其他结构 Money
:
struct Money
{
int Dollars;
int Cents;
};
这两种结构都具有几乎相似的数据成员。 与其重写不同的结构,不如将它放在一个地方会更好,这也将有助于:
您可能会说可以使用继承模型,在该模型中定义所有必需的方法,然后让派生类对其进行自定义。 它适合吗? 您选择的数据类型呢? 它可能是 int
, string
或 float
, 一些类 的类型 。 简而言之,继承只会使设计复杂化,而不会允许C ++模板促进插件功能。
在那里,我们使用类模板! 只需 定义 的类 模板即可 为两种类型 具有所有必需方法 。 开始吧!
template
struct Pair
{
// In public area, since we want the client to use them directly.
Type1 first;
Type2 second;
};
现在,我们可以使用 Pair
类模板来 派生 具有两个成员的任何类型。 一个例子:
// Assume as Point struct
Pair point1;
// Logically same as X and Y members
point1.first = 10;
point1.second = 20;
了解 类型 first
和 second
现在 是 int
和的 int
分别 。 这是因为我们 实例化 Pair
用这些类型 了。
当我们实例化它时:
Pair SqRoot;
SqRoot.first = 90;
SqRoot.second = 9.4868329;
first
将是 int
类型,并且 second
将是 double
类型。 清楚地了解 first
和 second
是数据成员,而不是函数,因此 不会对运行时造成任何损失 假定的 函数调用 。
注意 :在本文的此部分,所有定义仅在类声明主体内。 在下一部分中,我将解释如何在单独的实现文件中实现方法以及与此相关的问题。 因此,所示的所有方法定义都应仅假设在此范围内 class ClassName{...};
。
下面给出的默认构造函数初始化会成员都为它们的默认值,按数据类型 Type1
和 Type2
:
Pair() : first(Type1()), second(Type2())
{}
以下是带参数的构造函数,采用 Type1
和 Type2
初始化 值 first
and的 second
:
Pair(const Type1& t1, const Type2& t2) :
first(t1), second(t2)
{}
以下是一个复制构造函数,它将 复制一个 Pair
从另一个 对象 Pair
完全相同类型的对象 :
Pair(const Pair& OtherPair) :
first(OtherPair.first),
second(OtherPair.second)
{}
请注意,非常需要 指定的模板类型参数 Pair<>
为此复制构造函数的参数 。 下列规范没有意义,因为 Pair
它 不是 非模板类型:
Pair(const Pair& OtherPair) // ERROR: Pair requires template-types
这是一个使用参数化构造函数和copy-constructor的示例:
Pair point1(12,40);
Pair point2(point1);
重要的是要注意,如果您更改任何一个对象 任何模板类型参数 point2
或的 point1
,
则将无法使用 复制构造它 point1
object 。 以下将是一个错误:
Pair point2(point1); // ERROR: Different types, no conversion possible.
虽然,之间有可能转换 float
到 int
,但之间没有可能转换 Pair
到 Pair
。 复制构造函数不能将其他 类型 用作可复制对象。 有一个解决方案,但是我将在下一部分中讨论它。
您可以类似的方式实现比较运算符,以比较两个相同 对象 Pair
类型的 。 以下是等于操作符的实现:
bool operator == (const Pair& Other) const
{
return first == Other.first &&
second == Other.second;
}
请注意,我使用 const
属性,参数和方法本身。 请充分理解上述方法定义的第一行!
就像copy-constructor调用一样,您必须将完全相同的类型传递给此比较运算符-编译器不会尝试转换不同的 Pair
类型。 一个例子:
if (point1 == point2) // Both objects must be of same type.
...
为了对此处介绍的概念有扎实的理解,请自行实现以下方法:
Swap
方法Pair
class是两种类型的示例,可以使用它代替定义仅具有两个数据成员的多个结构。 缺点是只是记住什么 first
和 second
意味着(X或Y?)。 但是,当您很好地定义模板实例化时,您始终会 了解和使用 first
及其 second
适当地 成员。
忽略这一缺点,您将实现 中的所有功能 实例化 类型 :构造函数,复制构造函数,比较运算符,交换方法等。而且,无需重新编写各种两元结构所需的代码,您将获得所有这些。您将需要。 此外,如您所知,只有一组 必需的 方法会被编译和链接。 类模板中的错误修复将自动反映到所有实例中。 是的,如果修改不符合现有用法,则对类模板进行的轻微修改也可能会引发其他类型的错误。
同样,您可以具有一个类模板 模板 tuple
,该 允许三个(或更多)数据成员。 请尽量实现类 tuple
具有三个成员( first
, second
, third
)自己:
template
class tuple
好了,我们已经看到类模板和函数模板一样,可以采用多个类型参数。 但是类模板也允许很少的非类型模板参数。 在这一部分中,我将仅阐述一个非类型: integer 。
是的,类模板可以采用整数作为模板参数。 首先是一个样本:
template
class Array{};
在此类模板声明中, int SIZE
是一个非类型参数,它是一个整数。
int
, char
, long
, long long
, unsigned
变体和 enum
第 诸如 这样的类型 float
和 double
不允许使用 。100
, 100+99
, 1<<3
等是允许的,因为它们被编译时间常数表达式。 包含涉及函数调用的参数,例如 abs(-120)
不允许 。精细。 我们可以实例化类模板 Array
为:
Array my_array;
所以呢? 的目的是 SIZE
争论 什么?
好吧,在类模板中,可以在任何可能使用整数的地方使用此非类型整数参数。 这包括:
template
class Array
{
static const int Elements_2x = SIZE * 2;
};
[类声明的前两行将不再显示,假定所有内容都在类的主体之内。]
由于允许 初始化 static-constant-integer 在类声明中 ,因此我们可以使用非类型的整数参数。
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
请注意,第一个重载(声明为 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 Pair{};
该模板的实例是一个模板类:
Pair IntPair;
清醒地认识到 IntPair
是 不是 一个模板类,是 不是 对于类模板实例。 它是一个 对象 的特定实例/类模板的。 模板类/实例是 Pair
,它产生了另一个类类型(编译器,我们的朋友做到了,您知道!)。 本质上,这是在这种情况下编译器将生成的模板类:
class Pair{};
模板类有更精确的定义,请选择以下单行代码以便于理解。 详细的说明将在本系列的下一部分中进行。
现在,让我们说清楚。 如果您将模板类传递给某个类模板怎么办? 我的意思是,以下陈述意味着什么?
Pair > PairOfPair;
是否有效-如果是这样,这是什么意思?
首先,它是完全有效的。 其次,它实例化了 两个 模板类:
Pair
- 一个 PairPair >
- 乙 无论 一个 和 乙 类型会被编译器进行实例化,如果有任何错误,因为任何类型的这两个模板类的产生,编译器将报告。 为了简化这种 复杂的 实例,您可以执行以下操作:
typedef Pair IntIntPair;
...
Pair PairOfPair;
您可以这样分配 first
和 second
成员 PairOfPair
对象的 :
PairOfPair.first = 10;
PairOfPair.second.first = 10;
PairOfPair.second.second= 30;
请注意, second
最后两行中的member是type Pair
,因此它具有相同的一组成员以供进一步访问。 这就是原因 first
和 second
成员都可以使用,以 级联 的方式。
现在,您(希望)了解到类模板( Pair
)将模板类( Pair
)
作为参数并引入了最终实例化!
在此讨论中,一个有趣的实例将 Array
与一起使用 Pair
! 您知道 Pair
有两个模板类型参数, Array
一个类型参数和一个大小(整数)参数。
Array< Pair, 40> ArrayOfPair;
这是 int
和 double
的类型参数 Pair
。 因此, 的第一个模板类型 Array
(标记为粗体) 为 Pair
。 第二个参数是常量 40
。 您能回答这个问题吗:的构造函数会 Pair
被调用吗? 什么时候会被调用? 在您回答之前,我只是将实例反转为:
Pair> PairOfArray;
哇! 这是什么意思?
好吧,这意味着: PairOfArray
是的实例化 Pair
,将第一种类型作为 int
(对于 first
成员),而第二种类型( second
)是一个 Array
。 其中 Array
(的第二种类型 type的 Pair
)是 50
元素 double
!
不要为此而杀了我! 慢慢并清楚地了解模板的这些基本概念。 一旦获得了清晰的理解,您就会 喜欢 模板!
再一次,我使用了模板类( Array
)作为其他类型( 实例的参数 Pair
) 。
好的,但是 的右移运算符( >>
上面 )在做什么? 嗯,这不是运算符,而只是 Array
类型说明的结尾,然后是 的结尾 Pair
类型说明 。 一些旧的编译器要求我们在两个大于号之间插入一个空格,以避免出现错误或混乱。
Pair > PairOfArray;
当前,几乎所有现代C ++编译器都足够聪明,足以了解它用于结束模板类型规范,因此您不必担心。 因此,您可以随意使用两个或多个 >
符号来结束模板规范。
请 注意,在C ++术语中,传递模板类(实例化)并不是很具体-它只是类模板所采用的一种类型。
最后,在这里我将用法示例放到两个对象中。 首先是构造函数。
Array< Pair, 40> ArrayOfPair;
这将导致的构造函数 Pair
被调用 40 次,因为在 声明了常量大小的数组 Array
类模板中 :
T TheArray[SIZE];
这将意味着:
Pair TheArray[40];
因此,需要调用数量的构造函数 Pair
。
对于以下对象构造:
Pair> PairOfArray;
构造 Pair
将与初始化第一个参数 0
(使用 int()
符号),并且将呼叫的构造 Array
与 Array()
符号,如下图所示:
Pair() : first(int()), second(Array())
{}
由于 的默认构造函数 Array
类模板 由编译器提供,因此它将被调用。 如果您不理解此处编写的内容,请提高您的C ++技能。
分配的一个元素 ArrayOfPair
:
ArrayOfPair[0] = Pair(40, 3.14159);
在这里,您正在调用的非常量版本 版本 Array::operator[]
,该 将返回 的 引用 第一个元素 Array
(from TheArray
) 。 如您所知,该元素是type Pair
。 赋值运算符右侧的表达式只是调用构造函数, Pair
并传递所需的两个参数。 分配完成!
首先,让我消除与“默认参数”短语的任何歧义。 “功能模板”部分中使用了相同的短语。 在该小节中,默认参数是指功能参数本身的参数,而不是功能模板的类型参数。 无论如何,函数模板 不 支持模板参数的默认参数。 附带说明一下,请知道类模板的方法可以采用默认参数,就像任何普通函数/方法都可以采用那样。
另一方面,类模板确实为模板参数的type / non-type参数支持default-argument。 举个例子:
template
class Array
{
private:
T TheArray[SIZE];
...
};
我刚刚 了修改 SIZE
在类模板的第一行中进行 Array
。 第二个模板参数,即整数常量规范,现在设置为 100
。 这意味着,当您以以下方式使用它时:
Array IntArray;
从本质上讲,这意味着:
Array IntArray;
在实例化此类模板期间,编译器会自动将其放置。 当然,您可以通过显式传递第二个模板参数来指定自定义数组的大小:
Array IntArray;
请记住,当您通过类模板声明中指定的相同参数显式传递默认参数的参数时,它将仅实例化一次。 我的意思是,创建的以下两个对象将仅实例化一个类: Array
Array Array1;
Array Array2;
当然,如果您在类模板定义中更改默认的template参数(值为以外的值) 100
,则会导致两个模板实例化,因为它们是不同的类型。
您可以使用 自定义默认参数 const
或 #define
:
const int _size = 120;
// #define _size 150
template
class Array
当然,使用 _size
符号代替硬编码的常数表示相同。 但是使用符号会简化默认的'规范。 无论您如何为整数指定默认模板参数(这是一个非类型模板参数),它都必须是一个编译时间常数表达式。
你一般会 不 使用默认规范非类型整型参数,除非你正在使用的模板,先进的东西,如元编程,静态断言,SFINAE等,这肯定需要一个单独的部分。 更常见的是,您会看到并实现类模板的默认参数,即 数据类型 。 一个例子:
template
class Array100
{
T TheArray[100];
};
它定义了一个 类型的数组 T
size 100
。 在这里,type参数默认为 int
。 这意味着,如果您在实例化时未指定类型 Array100
,则它将映射到 int
。 以下是有关如何使用它的示例:
Array100 FloatArray;
Array100<> IntArray;
在第一个实例中,我 传递 float
以模板类型 ,而在第二个调用中,我 将其保留为默认值( int
使用 ) <>
符号 。 尽管此符号在模板编程中有更多用途,我将在后面的部分中进行介绍,但这种情况也非常需要。 如果您尝试将类模板用作:
Array100 IntArray;
这将导致编译器错误,即 Array100
需要模板参数。 因此, 必须使用尖括号( 空集 <>
如果所有模板参数均为默认值,并且您希望使用默认值,则 )的 实例化类模板。
要记住重要的事情是一个非模板类的名字 Array100
将 不会 被也是允许的。 如下所示,非模板类的定义以及模板类(彼此之间或之上或之下)的定义将使编译器不满意:
class Array100{}; // Array100 demands template arguments!
现在,让我们在类中混合使用type和non-type参数 Array
:
template
class Array
{
T TheArray[SIZE];
...
};
最后,type和size参数分别用 标记为default int
和 100
。 清楚地了解到,第一个 int
用于的默认规范 T
,第二个 int
用于非模板常量规范。 为了简化和提高可读性,应将它们放在不同的行中:
template
class Array{};
现在,使用您的智能来解析以下实例化的含义:
Array<> IntArray1;
Array IntArray2;
Array FlaotArray3;
就像 一样 函数模板中的显式说明 ,不允许仅指定尾随模板参数。 以下是错误:
Array<, 400> IntArrayOf500; // ERROR
最后,请记住,在创建两个对象之后将仅实例化一个类模板,因为它们实际上是完全相同的:
Array<> IntArray1;
Array IntArray2
Array IntArray3;
将模板类型默认为其他类型
也可以在先前到达的模板参数上默认设置type / non-type参数。 例如 ,我们可以修改 Pair
,如果未明确指定第二种类型 该类,以使第二种类型与第一种类型相同。
template
class Pair
{
Type1 first;
Type2 second;
};
在此修改后的类模板中 Pair
, Type2
现在默认为 Type1
type。 实例化的例子:
Pair IntPair;
您可以猜测,它与:
Pair IntPair;
但是,您不必输入第二个参数。 也可以将第一个参数 Pair
设为default:
template
class Pair
{
Type1 first;
Type2 second;
};
这意味着,如果你没有通过任何模板参数, Type1
会 int
,因此 Type2
也将是 int
!
用法如下:
Pair<> IntPair;
实例化以下类:
class Pair{};
当然,也可以在另一个非类型参数上默认非类型参数。 一个例子:
template
class Matrix
{
T TheMatrix[ROWS][COLUMNS];
};
但是, 从属 模板参数必须在 的 右边 其所依赖 。 以下将导致错误:
template
class Pair{};
template
class Matrix
虽然,这不是绝对的初学者,但是由于我同时介绍了函数模板和类模板,因此对该概念的阐述对于本系列文章的第一部分而言是合乎逻辑的。
考虑一个简单的例子:
class IntArray
{
int TheArray[10];
public:
template
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(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
作为 Copy
唯一的。 类的 方法 模板不过是嵌入在类中的普通功能模板。
请注意,我 使用 10
到处都 数组大小。 有趣的是,我们还可以将类修改为
template
class IntArray
{
int TheArray[ARRAY_SIZE];
public:
template
void Copy(T target_array[ARRAY_SIZE])
{
for(int nIndex = 0; nIndex(TheArray[nIndex]);
}
}
};
使得类 IntArray
和方法 Copy
,更好的候选人在模板编程领域!
就像您已经聪明地猜到的那样, Copy
方法只不过是一个数组转换例程,该例程可以从转换 int
为任何类型,只要 转换为 int
有可能就可以 给定类型。 这是一种有效的情况,其中类方法可以作为函数模板编写,可以自己获取模板参数。 请修改此类 模板 ,以使其可用于任何类型的数组,而不仅限于 int.
当然,带有方法模板的“显式模板参数指定”也是可能的。 考虑另一个示例:
template
class Convert
{
T data;
public:
Convert(const T& tData = T()) : data(tData)
{ }
template
bool IsEqualTo( const C& other ) const
{
return data == other;
}
};
可以用作:
Convert Data;
float Data2 = 1 ;
bool b = Data.IsEqualTo(Data2);
实例化 Convert::IsEqualTo
用 float
参数 。 如下所示,显式规范将使用实例化它 double
:
bool b = Data.IsEqualTo(Data2);
令人惊讶的事情之一是,借助模板,您可以通过在模板之上定义转换运算符来做到这一点!
template
operator T() const
{
return data;
}
将 Convert
只要有可能,就可以 '类模板实例转换为任何类型。 考虑以下用法示例:
Convert IntData(40);
float FloatData;
double DoubleData;
FloatData = IntData;
DoubleData = IntData;
它将实例化以下两种方法(完全限定的名称):
Convert::operator float();
Convert::operator double();
一方面,它提供了良好的灵活性,因为无需编写额外的代码, Convert
就可以将自身(特定的实例化)转换为任何数据类型-只要在编译级别可以进行转换即可。 如果无法进行转换,例如从 转换为 double
to 字符串类型,则会引发错误。
但是,另一方面,它也可能由于无意中插入错误而引起麻烦。 您可能不希望调用转换运算符,并且在您不了解转换运算符的情况下调用了该转换运算符(生成了编译器代码)。
您刚刚看到模板所提供的强大功能和灵活性。 下一部分将介绍更多高级和有趣的概念。 我对所有的读者谦逊和有抱负的要求是 发挥 与模板越来越多。 尝试首先在一个方面获得牢固的了解(仅像功能模板一样),而不是匆忙跳到其他概念。 最初是使用您的 测试 项目/代码库,而不是任何现有的/正在运行的/生产的代码。
以下是我们所涵盖内容的摘要:
希望您喜欢这篇文章,并清除了使模板变得复杂(不必要地是奇怪)的思想障碍。 第二部分将很快到达!