STL 是使用 C++ 提供的一种叫做模板的语言特性编写的。模板提供了一种方法,您可以用它来编写通用代码,这些代码可以在编译时被专门化,以创建具体的函数和不同类型的类。对模板代码的唯一要求是,可以为程序中用于专门化模板的所有类型生成输出。在这一点上,这可能有点难以理解,但是当你读完这一章的时候,你就会明白了。
9-1.创建模板函数
问题
您希望创建一个函数,可以传递不同类型的参数并返回不同类型的值。
解决办法
可以使用方法重载为您希望支持的每种类型提供不同版本的函数,但这仍然会将您限制在所提供类型的函数中。更好的方法是创建一个模板函数,专门用于任何类型。
它是如何工作的
C++ 包括一个模板编译器,可以用来在编译时将通用函数定义转换成具体函数。
创建模板函数
模板允许您在不指定具体类型的情况下编写代码。代码通常包含您希望使用的类型;清单 9-1 显示了在这些正常情况下编写的函数。
清单 9-1 。 非模板功能
#include <iostream>
using namespace std;
int Add(int a, int b)
{
return a + b;
}
int main(int argc, char* argv[])
{
const int number1{ 1 };
const int number2{ 2 };
const int result{ Add(number1, number2) };
cout << "The result of adding" << endl;
cout << number1 << endl;
cout << "to" << endl;
cout << number2 << endl;
cout << "is" << endl;
cout << result;
return 0;
}清单 9-1 中的Add函数是一个标准的 C++ 函数。它接受两个int参数并返回一个int值。您可以提供这个函数的一个float版本,方法是复制这个函数并修改每个对int的引用,以便它使用一个float来代替。然后,您可以对string和您希望该函数支持的任何其他类型进行同样的操作。这种方法的问题是,即使函数体保持不变,也必须为每种类型复制函数。另一种解决方案是使用模板函数。你可以在清单 9-2 中看到Add的模板版本。
**清单 9-2 。**一个Add的模板版本
template <typename T>
T Add(const T& a, const T& b)
{
return a + b;
}可以看到,Add的模板版本不再使用具体类型int。相反,该函数是在模板块中定义的。template关键字用来告诉编译器下一个代码块应该被当作一个模板。接下来是尖括号部分(< >),它定义了模板使用的任何类型。这个例子定义了一个模板类型,用字符T. T表示,然后用来指定返回类型和传递给函数的两个参数的类型。
注意将参数作为const引用传递给模板函数是个好主意。最初的Add实现通过值传递int类型,但是不能保证模板不会被在通过值传递时会造成性能损失的类型使用,比如复制的对象。
现在你已经模板化了Add函数,你可以在清单 9-3 中看到main函数中的调用代码与清单 9-1 中显示的代码没有什么不同。
清单 9-3 。 调用模板Add功能
#include <iostream>
using namespace std;
template <typename T>
T Add(const T& a, const T& b)
{
return a + b;
}
int main(int argc, char* argv[])
{
const int number1{ 1 };
const int number2{ 2 };
const int result{ Add(number1, number2) };
cout << "The result of adding" << endl;
cout << number1 << endl;
cout << "to" << endl;
cout << number2 << endl;
cout << "is" << endl;
cout << result;
return 0;
}清单 9-3 包含了一个对Add函数的调用,其位置与清单 9-1 中的代码完全相同。这是可能的,因为编译器可以隐式地计算出与模板一起使用的正确类型。
显式与隐式模板专门化
有时,您希望明确模板可以使用的类型。清单 9-4 显示了一个显式模板专门化的例子。
清单 9-4 。 显性和隐性模板特殊化
#include <iostream>
using namespace std;
template <typename T>
T Add(const T& a, const T& b)
{
return a + b;
}
template <typename T>
void Print(const T& value1, const T& value2, const T& result)
{
cout << "The result of adding" << endl;
cout << value1 << endl;
cout << "to" << endl;
cout << value2 << endl;
cout << "is" << endl;
cout << result;
cout << endl << endl;
}
int main(int argc, char* argv[])
{
const int number1{ 1 };
const int number2{ 2 };
const int intResult{ Add(number1, number2) };
Print(number1, number2, intResult);
const float floatResult{ Add(static_cast<float>(number1), static_cast<float>(number2)) };
Print<float>(number1, number2, floatResult);
return 0;
}清单 9-4 添加了一个带三个模板化参数的模板Print函数。该函数在main函数中被调用两次。第一次是隐式推导模板类型。这是可能的,因为传递给函数的三个参数都是类型int;因此,编译器认为您打算调用模板的一个int版本。对Print的第二个调用是显而易见的。这是通过在函数名后面添加包含要使用的类型的尖括号(在本例中是float)来实现的。由于传递给函数的变量类型不同,这是必要的。这里number1和number2都是int类型,但是floatResult是float类型;因此,编译器无法推断出模板使用的正确类型。当我尝试使用隐式专用化编译此代码时,Visual Studio 生成了以下错误:
error C2782: 'void Print(const T &,const T &,const T &)' : template parameter 'T' is ambiguous9-2.部分专门化模板
问题
你有一个不能用特定类型编译的模板函数。
解决办法
您可以使用部分模板专门化来创建模板重载。
它是如何工作的
模板函数体包含需要隐式属性的代码,这些隐式属性来自用于专门化该模板的类型。考虑清单 9-5 中的代码。
清单 9-5 。 模板功能
#include <iostream>
using namespace std;
template <typename T>
T Add(const T& a, const T& b)
{
return a + b;
}
template <typename T>
void Print(const T& value1, const T& value2, const T& result)
{
cout << "The result of adding" << endl;
cout << value1 << endl;
cout << "to" << endl;
cout << value2 << endl;
cout << "is" << endl;
cout << result;
cout << endl << endl;
}
int main(int argc, char* argv[])
{
const int number1{ 1 };
const int number2{ 2 };
const int intResult{ Add(number1, number2) };
Print(number1, number2, intResult);
return 0;
}这段代码需要来自Add函数和Print函数使用的类型的两个隐式属性。Add功能要求使用的类型也可以与+操作符一起使用。Print函数要求使用的类型可以传递给<<操作符。main函数使用这些带有int变量的函数,因此这两个条件都满足。如果您要对自己创建的类使用Add或Print,那么编译器很可能无法使用带有+或<<操作符的类。T13】
注意这种情况下“合适的”解决方案是添加重载的+和<<操作符,这样原始代码就能按预期工作。这个例子展示了如何使用部分专门化来达到同样的结果。
你可以很容易地更新清单 9-5 中的来使用一个简单的类,如清单 9-6 中的所示。
清单 9-6 。 使用带类的模板
#include <iostream>
using namespace std;
class MyClass
{
private:
int m_Value{ 0 };
public:
MyClass() = default;
MyClass(int value)
: m_Value{ value }
{
}
MyClass(int number1, int number2)
: m_Value{ number1 + number2 }
{
}
int GetValue() const
{
return m_Value;
}
};
template <typename T>
T Add(const T& a, const T& b)
{
return a + b;
}
template <typename T>
void Print(const T& value1, const T& value2, const T& result)
{
cout << "The result of adding" << endl;
cout << value1 << endl;
cout << "to" << endl;
cout << value2 << endl;
cout << "is" << endl;
cout << result;
cout << endl << endl;
}
int main(int argc, char* argv[])
{
const MyClass number1{ 1 };
const MyClass number2{ 2 };
const MyClass intResult{ Add(number1, number2) };
Print(number1, number2, intResult);
return 0;
}清单 9-6 中的代码无法编译。你的编译器将找不到合适的操作符来为+和<<使用MyClass类型。你可以通过使用部分模板专门化来解决这个问题,如清单 9-7 所示。
清单 9-7 。 使用分部分项模板特殊化
#include <iostream>
using namespace std;
class MyClass
{
private:
int m_Value{ 0 };
public:
MyClass() = default;
MyClass(int value)
: m_Value{ value }
{
}
MyClass(int number1, int number2)
: m_Value{ number1 + number2 }
{
}
int GetValue() const
{
return m_Value;
}
};
template <typename T>
T Add(const T& a, const T& b)
{
return a + b;
}
template <>
MyClass Add(const MyClass& myClass1, const MyClass& myClass2)
{
return MyClass(myClass1.GetValue(), myClass2.GetValue());
}
template <typename T>
void Print(const T& value1, const T& value2, const T& result)
{
cout << "The result of adding" << endl;
cout << value1 << endl;
cout << "to" << endl;
cout << value2 << endl;
cout << "is" << endl;
cout << result;
cout << endl << endl;
}
template <>
void Print(const MyClass& value1, const MyClass& value2, const MyClass& result)
{
cout << "The result of adding" << endl;
cout << value1.GetValue() << endl;
cout << "to" << endl;
cout << value2.GetValue() << endl;
cout << "is" << endl;
cout << result.GetValue();
cout << endl << endl;
}
int main(int argc, char* argv[])
{
const MyClass number1{ 1 };
const MyClass number2{ 2 };
const MyClass intResult{ Add(number1, number2) };
Print(number1, number2, intResult);
return 0;
}清单 9-7 中的代码增加了Add和Print的特殊版本。它通过在函数签名中使用一个空的模板类型说明符和具体的MyClass类型来实现。您可以在Add函数中看到这一点,这里传递的参数属于MyClass类型,返回值属于MyClass类型。部分专门化的Print函数也将const引用传递给MyClass变量。模板函数仍然可以和变量一起使用,比如int s 和float s,但是现在也明确支持MyClass类型。
为了完整起见,清单 9-8 显示了一个优选的实现,它增加了对+和<<操作符和MyClass的支持。
清单 9-8 。 增加+和<<操作员支持到MyClass
#include <iostream>
using namespace std;
class MyClass
{
friend ostream& operator <<(ostream& os, const MyClass& myClass);
private:
int m_Value{ 0 };
public:
MyClass() = default;
MyClass(int value)
: m_Value{ value }
{
}
MyClass(int number1, int number2)
: m_Value{ number1 + number2 }
{
}
MyClass operator +(const MyClass& other) const
{
return m_Value + other.m_Value;
}
};
ostream& operator <<(ostream& os, const MyClass& myClass)
{
os << myClass.m_Value;
return os;
}
template <typename T>
T Add(const T& a, const T& b)
{
return a + b;
}
template <typename T>
void Print(const T& value1, const T& value2, const T& result)
{
cout << "The result of adding" << endl;
cout << value1 << endl;
cout << "to" << endl;
cout << value2 << endl;
cout << "is" << endl;
cout << result;
cout << endl << endl;
}
int main(int argc, char* argv[])
{
const MyClass number1{ 1 };
const MyClass number2{ 2 };
const MyClass intResult{ Add(number1, number2) };
Print(number1, number2, intResult);
return 0;
}这段代码直接为MyClass添加了对+操作符的支持。还为与ostream类型一起工作的<<操作符指定了一个功能。这是因为cout与ostream(代表输出流)兼容。该函数签名作为MyClass的friend添加,以便函数可以从MyClass访问内部数据。您也可以保留GetValue访问器,而不添加操作符作为friend函数。
9-3.创建课程模板
问题
您希望创建一个可以存储不同类型变量的类,而无需复制所有代码。
解决办法
C++ 允许创建支持抽象类型的模板类。
它是如何工作的
您可以使用template说明符将class定义为模板。template说明符将类型和值作为编译器用来构建模板代码专门化的参数。清单 9-9 展示了一个使用抽象类型和值来构建模板类的例子。
清单 9-9 。 创建模板类
#include <iostream>
using namespace std;
template <typename T, int numberOfElements>
class MyArray
{
private:
T m_Array[numberOfElements];
public:
MyArray()
: m_Array{}
{
}
T& operator[](const unsigned int index)
{
return m_Array[index];
}
};
int main(int argc, char* argv[])
{
const unsigned int ARRAY_SIZE{ 5 };
MyArray<int, ARRAY_SIZE> myIntArray;
for (unsigned int i{ 0 }; i < ARRAY_SIZE; ++i)
{
myIntArray[i] = i;
}
for (unsigned int i{ 0 }; i < ARRAY_SIZE; ++i)
{
cout << myIntArray[i] << endl;
}
cout << endl;
MyArray<float, ARRAY_SIZE> myFloatArray;
for (unsigned int i{ 0 }; i < ARRAY_SIZE; ++i)
{
myFloatArray[i] = static_cast<float>(i)+0.5f;
}
for (unsigned int i{ 0 }; i < ARRAY_SIZE; ++i)
{
cout << myFloatArray[i] << endl;
}
return 0;
}class MyArray创建一个类型为T的 C 风格数组和一些元素。这两者在编写类时是抽象的,在代码中使用它们时是指定的。您现在可以使用MyArray类来创建一个任意大小的数组,其中包含任意数量的元素,这些元素可以用一个int来表示。你可以在main函数中看到这一点,其中MyArray class模板专门创建了一个int的数组和一个float的数组。图 9-1 显示了运行这段代码时生成的输出:这两个数组包含不同类型的变量。
注意数组模板包装器的创建是一个简单的例子,展示了 STL 提供的std::array模板的基础。STL 版本支持 STL 迭代器和算法,是比自己编写实现更好的选择。
9-4.创建单件
问题
您有一个系统,您想创建一个可以从应用程序的许多地方访问的实例。
解决办法
你可以使用模板创建一个Singleton基类 。
它是如何工作的
singleton 的基础是一个类模板。Singleton类模板包含一个指向抽象类型的static指针,可以用来表示你喜欢的任何类型的类。使用static指针的副产品是可以从程序的任何地方访问类的实例。您应该小心不要滥用它,尽管它可能是一个有用的属性。清单 9-10 展示了如何创建和使用Singleton模板。
清单 9-10 。Singleton模板
#include <cassert>
#include <iostream>
using namespace std;
template <typename T>
class Singleton
{
private:
static T* m_Instance;
public:
Singleton()
{
assert(m_Instance == nullptr);
m_Instance = static_cast<T*>(this);
}
virtual ~Singleton()
{
m_Instance = nullptr;
}
static T& GetSingleton()
{
return *m_Instance;
}
static T* GetSingletonPtr()
{
return m_Instance;
}
};
template <typename T>
T* Singleton<T>::m_Instance = nullptr;
class Manager
: public Singleton < Manager >
{
public:
void Print() const
{
cout << "Singleton Manager Successfully Printing!";
}
};
int main(int argc, char* argv[])
{
new Manager();
Manager& manager{ Manager::GetSingleton() };
manager.Print();
delete Manager::GetSingletonPtr();
return 0;
}清单 9-10 中的Singleton类是一个模板类,它包含一个指向抽象类型 t 的私有静态指针。Singleton构造函数将this的造型赋给m_Instance变量。以这种方式使用static_cast是可能的,因为您知道对象的类型将是提供给模板的类型。该类的虚拟析构函数负责将m_Instance设置回nullptr;还有对实例的引用和指针访问器。
清单 9-10 然后使用这个模板创建一个支持Singleton的Manager类。它通过创建一个继承自Singleton的类并将其自身类型传递给Singleton模板参数来实现这一点。
注意将一个类的类型传递到该类派生自的模板中被称为奇怪的递归模板模式。
main函数使用new关键字创建一个Manager。Manager不是作为类的引用或指针存储的。虽然您可以这样做,但是从这一点来看,最好简单地使用Singleton的访问器。您可以通过使用带有派生类名称的静态函数语法来实现这一点。main函数通过调用Manager::GetSingleton函数创建对Manager实例的引用。
通过对由Manager::GetSingletonPtr返回的值调用delete来删除单例实例。这会导致调用~Singleton,这将清除存储在m_Instance中的地址,并释放用于存储实例的内存。
注这个Singleton类是基于 Scott Bilas 在游戏编程宝石 (Charles River Media,2000)中最初写的实现。
9-5.编译时计算值
问题
您需要计算复杂的值,并且希望避免在运行时计算它们。
解决办法
模板元编程利用 C++ 模板编译器在编译时计算值,并为用户节省运行时性能。
它是如何工作的
模板元编程可能是一个很难理解的话题。这种复杂性来自 C++ 模板编译器的能力范围。除了让您通过从函数和类中抽象类型来执行泛型编程之外,模板编译器还可以计算值。
散列数据是比较两组数据是否相等的常用方法。它的工作原理是在创建时创建数据的散列,并将散列与数据的运行时版本进行比较。您可以使用此方法在程序执行时检测数据文件的可执行文件中的更改。SDBM 散列是一个易于实现的散列函数;清单 9-11 显示了 SDBM 散列算法 的一个普通函数实现。
***清单 9-11 。***SDBM 哈希算法
#include <iostream>
#include <string>
using namespace std;
unsigned int SDBMHash(const std::string& key)
{
unsigned int result{ 0 };
for (unsigned int character : key)
{
result = character + (result << 6) + (result << 16) - result;
}
return result;
}
int main(int argc, char* argv[])
{
std::string data{ "Bruce Sutherland" };
unsigned int sdbmHash{ SDBMHash(data) };
cout << "The hash of " << data << " is " << sdbmHash;
return 0;
}清单 9-11 中的SDBMHash函数的工作方式是迭代提供的数据,并通过将数据集中的每个字节处理成一个result变量来计算结果。这个功能版本的SDBMHash对于创建运行时加载的数据的散列是有用的,但是这里提供的数据在编译时是已知的。通过用模板元程序替换这个函数,可以优化程序的执行速度。清单 9-12 就是这么做的。
清单 9-12 。 用模板元程序替换SDBMHash
#include <iostream>
using namespace std;
template <int stringLength>
struct SDBMCalculator
{
constexpr static unsigned int Calculate(const char* const stringToHash, unsigned int& value)
{
unsigned int character{
SDBMCalculator<stringLength - 1>::Calculate(stringToHash, value)
};
value = character + (value << 6) + (value << 16) - value;
return stringToHash[stringLength - 1];
}
constexpr static unsigned int CalculateValue(const char* const stringToHash)
{
unsigned int value{};
unsigned int character{ SDBMCalculator<stringLength>::Calculate(stringToHash, value) };
value = character + (value << 6) + (value << 16) - value;
return value;
}
};
template<>
struct SDBMCalculator < 1 >
{
constexpr static unsigned int Calculate(const char* const stringToHash, unsigned int& value)
{
return stringToHash[0];
}
};
constexpr unsigned int sdbmHash{ SDBMCalculator<16>::CalculateValue("Bruce Sutherland") };
int main(int argc, char* argv[])
{
cout << "The hash of Bruce Sutherland is " << sdbmHash << endl;
return 0;
}您可以立即看到清单 9-12 中的代码看起来比清单 9-11 中的代码复杂得多。编写模板元程序所需的语法不是最容易读懂的。main函数现在是单行代码。哈希值存储在一个常量中,不调用任何模板函数。您可以通过在模板函数中放置断点并运行程序的发布版本来测试这一点。
清单 9-12 中的模板元程序通过使用递归来工作。要散列的数据的长度被提供给模板参数,并且可以在初始化sdbmHash变量时看到。这里,16传递给模板,就是字符串“Bruce Sutherland ”的长度。模板编译器认识到它已经被提供了可以在编译时评估的数据,因此它自动调用CalculateValue函数中的Calculate元程序函数。这种递归一直发生,直到碰到终止符。终止符是Calculate的部分专门化版本,一旦要散列的数据长度为 1,就会被调用。当到达终止符时,递归调用开始展开,编译器最终将模板元程序的结果存储在sdbmHash变量中。您可以使用调试版本看到模板元程序的运行。编译器不会在调试版本中优化模板元程序,调试版本允许您测试代码并单步执行以查看结果。图 9-2 显示了运行清单 9-12 中代码的输出。

