C++模版小记:从入门到入土
- 附录1. c++模版类 - 从入门到入土
- 第一关:为什么需要模版类?
- 第二关:「函数模版」
- 第三关:「类模版」
- 第四关:「多模板参数」与「非类型参数」
- 第五关:「模板特化」
- 第六关:「类型推断」
- 第七关:「变量模板」
- 第八关:「模板类型别名」
- 第九关:模板的SFINAE原则
- 第十关:模板与友元
- 第十一关:折叠表达式
- 第十二关:模板概念(Concepts) - C++20
- 第十三关:
std::enable_if
和 SFINAE - 第十四关:类模板偏特化
- 第十五关:
constexpr
和模板 - 第十六关:模板中的嵌套类型
- 第十七关:模板参数包与展开
- 第十八关:Lambda 表达式与模板
- 第十九关:模板递归
- 第二十关:带有模板的继承
- 第二十一关:
std::type_trait
的工具集 - 第二十二关:模板与动态多态性
本文是专门为了我另一篇文章光栅化渲染器而写的。
第一关:为什么需要模版类?
第二关:「函数模版」
第三关:「类模版」
第四关:「多模板参数」与「非类型参数」
第五关:「模板特化」
第六关:「类型推断」
1.auto & decltype
2.模板中的基本类型推断
3.自动构造模版类型
4.尾返回类型
第七关:「变量模板」
第八关:「模板类型别名」
第九关:模板的SFINAE原则
第十关:模板与友元
第十一关:折叠表达式
第十二关:模板概念 - C++20
第十三关: std::enable_if
和 SFINAE
第十四关:类模板偏特化
第十五关:constexpr
和模板
第十六关:模板中的嵌套类型
第十七关:模板参数包与展开
第十八关:Lambda 表达式与模板
第十九关:模板递归
第二十关:带有模板的继承
附录1. c++模版类 - 从入门到入土
第一关:为什么需要模版类?
第二关:「函数模版」
第三关:「类模版」
第四关:「多模板参数」与「非类型参数」
第五关:「模板特化」
第六关:「类型推断」
1. auto & decltype 2. 模板中的基本类型推断3. 自动构造模版类型4. 尾返回类型
第七关:「变量模板」
第八关:「模板类型别名」
第九关:模板的SFINAE原则
第十关:模板与友元
第十一关:折叠表达式
第十二关:模板概念 - C++20
第十三关: std::enable_if
和 SFINAE
第十四关:类模板偏特化
第十五关:constexpr
和模板
第十六关:模板中的嵌套类型
第十七关:模板参数包与展开
第十八关:Lambda 表达式与模板
第十九关:模板递归
第二十关:带有模板的继承
第一关:为什么需要模版类?
在没有模板之前,如果你想为不同的数据类型编写相同的功能,你可能需要为每种数据类型写一个函数或类。这会导致大量的重复代码。
用专业的话来说就是,函数模板和类模板在 C++ 中是用来支持泛型编程的工具。泛型编程是一种编写与类型无关的代码的方法。这就意味着,通过使用模板,你可以创建一个能够适应任何数据类型的函数或类,而不需要为每种数据类型都重新编写代码。
例如一个函数,它的任务是交换两个整数的值。后来,你又想交换两个浮点数。没有模板,你可能需要为每种数据类型编写单独的函数。
第二关:「函数模版」
解决上面提到的问题,非常简单。
// 模版函数
template <typename T>
void swap(T &a, T &b) {
T temp = a;
a = b;
b = temp;
}
// 调用方法
int x = 5, y = 10;
swap(x, y);
double m = 5.5, n = 10.5;
swap(m, n);
template <typename T>
声明了一个模板函数。此处的 T
可以被认为是一个占位符,它在编译时会被实际的数据类型替换。
第三关:「类模版」
类模版跟函数模版差不多。下面的例子是一个用于存储任意类型的数组的类。
// 模版类
template <typename T>
class Array {
private:
T *data;
int size;
public:
Array(int s) : size(s) {
data = new T[size];
}
~Array() {
delete[] data;
}
T& operator[](int index) { // 实现索引获取元素
return data[index];
}
};
// 如何调用?
Array<int> intArray(10);
Array<double> doubleArray(10);
第四关:「多模板参数」与「非类型参数」
可以为一个模板定义多个参数。同时,参数可以是上面所说的 typename T
非类型参数,也可以是类型参数,像下面代码中的 int SIZE
。
// 多模板参数、非类型参数
template <typename T, int SIZE>
class FixedArray {
private:
T data[SIZE];
public:
T& operator[](int index) {
return data[index];
}
}
// 使用方式
FixedArray<int, 10> intArray;
第五关:「模板特化」
有时候,希望某个模板对某个特定类型有一个不同的实现。这时你可以使用模板特化。假如现在有下面的模版。
template <typename T>
class Printer {
public:
void print(T value) {
std::cout << "General print: " << value << std::endl;
}
};
但我希望对于 int
类型有一个特殊的输出。
template <>
class Printer<int> {
public:
void print(int value) {
std::cout << "Special print for int: " << value << std::endl;
}
};
第六关:「类型推断」
1. auto & decltype
在 C++11 中引入了很多特性,其中一个与类型推断相关的特性是“auto”关键字。除了刚才说的“auto”,C++11还引入了“decltype”关键词,可以判断一个表达式的类型。
auto x = 42; // x 的类型被推断为 int
auto y = 3.14; // y 的类型被推断为 double
int num = 5;
decltype(num) y = 10; // y 的类型被推断为 int
2. 模板中的基本类型推断
此外,函数模板的类型推断在 C++ 中已经存在了一段时间,但 C++11 增强了这一特性。函数模板可以自动推断类型参数。
template <typename T>
void show(T value) {
std::cout << value << std::endl;
}
// 调用
show(5); // 5
show(3.14); // 3.14
3. 自动构造模版类型
在 C++17 之后,类型推断就更加强大了。在 C++17 之前,类模板的类型参数不能自动推断。但是从 C++17 开始,我们可以通过模板参数的自动类型推断来构造类模板的对象。
template <typename T>
class MyClass {
T data;
public:
MyClass(T d) : data(d) {}
void display() {
std::cout << data << std::endl;
}
};
int main() {
// C++17 之前的方式
MyClass<int> obj1(10);
obj1.display();
// C++17 之后的方式
MyClass obj2(10); // 自动推断为 MyClass<int>
obj2.display();
}
4. 尾返回类型
C++11 引入了尾返回类型,使得函数的返回类型可以基于其参数进行推断,这对于模板特别有用。下面代码的 ->
用于指定函数的尾返回类型。此时,auto
告诉编译器函数返回类型将由其后的表达式来决定,也就是刚刚说的 ->
。
template <typename T1, typename T2>
auto add(T1 x, T2 y) -> decltype(x + y) {
return x + y;
}
int main() {
auto result = add(5, 3.14); // 结果的类型推断为 double
std::cout << result << std::endl;
}
第七关:「变量模板」
C++14 引入了变量模板,它允许你为模板定义静态数据成员。它与函数和类的模板类似,但是用于变量。
我们定义了一个名为 pi
的变量模板,它为每种类型 T
提供了 π 的近似值。你可以像使用其他模板那样使用变量模板,但需要指定模板参数来获取相应的变量实例。
template <typename T>
constexpr T pi = T(3.1415926535897932385);
int main() {
std::cout << pi<int> << std::endl; // 输出 3
std::cout << pi<double> << std::endl; // 输出 3.14159...
}
一般这个「变量模版」非常适用于那些需要为不同类型提供不同值或配置的情况。同时使用的时候注意以下事项:
- 变量模板通常与
constexpr
一起使用,以确保它们在编译时是常数。 - 变量模板的实例化方式与函数或类模板相似。当你第一次为特定的类型使用变量模板时,编译器将为该类型创建一个实例。
第八关:「模板类型别名」
「模板类型别名」为已存在的模板类型定义了一个新的、更简短的名称。
在 C++11 之前,如果你想为复杂的模板类型创建别名,这往往是非常麻烦的。C++11 引入了 using
关键字来创建模板类型别名,这提供了一个更清晰、更简洁的方式来定义这些别名。
这里以 第三关 的例子说明创建别名的最简单实践。
template <typename T>
using MyArray = Array<T>;
这里再举一个简单、常用的例子为常见的向量类型提供别名。
using Vec3f = Vec3<float>;
using Vec3d = Vec3<double>;
using Vec4f = Vec4<float>;
using Vec4d = Vec4<double>;
Vec3f position;
值得注意的是,你也可以用old school的方法,即typedef。上下两段代码是完全一致的。
typedef Vec2<float> Vec2f;
typedef Vec2<int> Vec2i;
typedef Vec3<float> Vec3f;
typedef Vec3<int> Vec3i;
他们的区别在于,typedef
使用旧的 C/C++ 语法,而using
是 C++11 引入的新语法,用于定义类型别名。对于简单的类型别名,这两种方法之间的差异可能不明显。但是,当涉及到更复杂的类型,如函数指针或模板类型,using
的语法往往更为简洁和直观。
这里拓展一下,
using
和typedef
两者一个主要的区别是,using
可以为模板提供别名。template <typename T> using Vec2Ptr = Vec2<T>*;
第九关:模板的SFINAE原则
SFINAE 原则是 C++ 模板中的一个特性。SFINAE是“Substitution Failure Is Not An Error”(替换失败不是错误)的缩写。当试图用给定的模板参数替换模板时,如果发生错误,则该特殊化不被考虑。
想象一下你正在为一个魔法展示准备一套卡片。每张卡片上都有一个指令,例如“变成兔子”或“飞起来”。但有一张卡片的指令是“让猪飞起来”。显然,这是一个不可能的任务。
在通常情况下,魔术师会看到这张卡片并说:“这个指令有问题,展示失败了!”。但在 SFINAE 的世界里,魔术师会说:“好吧,这张卡片不工作,让我试试下一张”。
换句话说,SFINAE 就像是编译器的一个内置魔术师。当你尝试用一个不合适的类型进行模板替换时,而不是直接报错,编译器会悄悄地“忽略”那个模板,并尝试其他的选项。
直到没有选项合适(No matching)或者很多合适选项(Ambiguous),编译器就会报出错误。
一个简单的场景:我们希望写一个函数 printValue
,该函数可以打印整数或字符串。但是,如果我们尝试使用其他类型,这个函数就不应该存在。
#include <iostream>
#include <type_traits>
// 1. 对于整数类型
template <typename T>
typename std::enable_if<std::is_integral<T>::value>::type
printValue(const T& val) {
std::cout << "Integer: " << val << std::endl;
}
// 2. 对于字符串类型
template <typename T>
typename std::enable_if<std::is_same<T, std::string>::value>::type
printValue(const T& val) {
std::cout << "String: " << val << std::endl;
}
int main() {
printValue(42); // 输出: Integer: 42
printValue(std::string("Hello")); // 输出: String: Hello
// printValue(3.14); // 这一行会引起编译错误,因为没有适合double类型的printValue版本
return 0;
}
这一长串代码确实有点丑陋了,我们将代码拆开详细看看。
// 节选自上面的代码
template <typename T>
typename std::enable_if<std::is_integral<T>::value>::type
printValue(const T& val) {
std::cout << "Integer: " << val << std::endl;
}
模板声明:
template <typename T>
声明了一个模板函数,其中
T
是一个待定的类型。你可以为T
提供任何类型,比如int
、double
、std::string
等,但是函数的实际行为取决于你提供的类型。返回类型:
typename std::enable_if<std::is_integral<T>::value>::type
这段代码使用了两个主要的模板工具:
std::enable_if
和std::is_integral
。std::is_integral<T>::value
是一个类型特性,检查T
是否是整数类型。如果是,它返回true
;否则返回false
。std::enable_if
是一个模板,它有一个嵌套的type
成员,但这个成员只在给定的布尔表达式为true
时存在。在这里,它检查前面的std::is_integral<T>::value
是否为true
。
结合起来,这意味着:
- 如果
T
是整数类型,函数的返回类型将是void
(因为std::enable_if
的默认类型是void
)。 - 如果
T
不是整数类型,由于type
成员不存在,SFINAE 将阻止此函数模板被实例化,因此该版本的printValue
函数将不可用。
如果我想让当函数传入int类型时输出double类型,可以这样做:
template <typename T>
typename std::enable_if<std::is_same<T, int>::value, double>::type
printValue(const T& val) {
std::cout << "Integer: " << val << std::endl;
return static_cast<double>(val);
}
int main() {
double result = printValue(42);
std::cout << "Returned value: " << result << std::endl;
// print 42.0
}
关键部分是 typename std::enable_if<std::is_same<T, int>::value, double>::type
,这会检查 T
是否与 int
相同。如果是,它将产生类型 double
。如果不是,该版本的 printValue
函数将由于 SFINAE 而不被考虑。
有朋友可能会说,为什么不用多态呢?写这坨代码实在是太难看了,我用多态写那叫一个简洁:
double printValue(int val) {
std::cout << "Integer: " << val << std::endl;
return static_cast<double>(val);
}
void printValue(double val) {
std::cout << "Double: " << val << std::endl;
}
以下是一些常见的解释:
- 泛型编程: 使用模板,你可以为各种类型编写通用的代码,而不仅仅是那些你预先知道的类型。
- 类型约束: 通过 SFINAE 和其他模板技巧,你可以对哪些类型可以用于你的泛型代码施加更精细的约束。例如,你可能想要一个函数,它只接受具有某些成员函数的对象。
- 编译时优化: 由于模板在编译时实例化,编译器可以为每个特定的类型生成优化过的代码,这可能会导致更高的执行效率。
- 灵活性: 模板提供了更多的灵活性,例如模板元编程、模板特化等,允许更复杂和高效的编程技术。
- 类型透明性: 当使用模板时,原始类型信息在使用模板函数或类的地方保持不变。这与多态不同,其中类型信息可能会丢失,特别是在使用继承和虚函数时。
随着进一步学习以及项目的接触,我们可以更加体会到这种编程方式的优缺点。
第十关:模板与友元
模板类或函数可以声明为另一个类或函数的友元。
template <typename T>
class Container {
private:
T data;
public:
Container(T d) : data(d) {}
template <typename U>
friend bool operator==(const Container<U>&, const Container<U>&);
};
template <typename T>
bool operator==(const Container<T>& lhs, const Container<T>& rhs) {
return lhs.data == rhs.data;
}
第十一关:折叠表达式
C++17中的折叠表达式可以简化某些变长模板参数的操作。
例如,要计算所有给定参数的总和:
template<typename... Args>
auto sum(Args... args) {
return (... + args);
}
第十二关:模板概念(Concepts) - C++20
C++20引入了模板的概念,允许你为模板参数指定更明确的约束。只有满足给定概念的类型才可以作为print
函数的参数。
比如说,
template <typename T>
concept Arithmetic = std::is_arithmetic_v<T>;
template <Arithmetic T>
T add(T a, T b) {
return a + b;
}
这里是其他的一些特性:
std::is_integral<T>
:检查T
是否是整数类型。std::is_floating_point<T>
:检查T
是否是浮点数类型。std::is_array<T>
:检查T
是否是数组。std::is_pointer<T>
:检查T
是否是指针。std::is_reference<T>
:检查T
是否是引用。std::is_class<T>
:检查T
是否是类或结构体类型。std::is_function<T>
:检查T
是否是函数。
另外还可以通过其他方法检查“一个类型是否可以被输出流输出”。也就是在下面代码中,我们定义了一个Printable
的conecpt,要满足这个概念,类型 T
必须满足 requires
表达式中的要求。
template <typename T>
concept Printable = requires(T t) {
{ std::cout << t } -> std::same_as<std::ostream&>;
};
template <Printable T>
void print(T value) {
std::cout << value;
}
其中, requires
表达式是与概念 (concepts) 相关的一种新特性,用于描述一个类型必须满足的要求。
requires ( 参数 ) { 要求列表 }
在这里,我们要求类型 T
必须支持一个操作,即:当你尝试将 t
输出到 std::cout
时,结果的类型必须是 std::ostream&
。在 requires
表达式中,->
符号被用于指定一个表达式的预期返回类型。
另外注意,requires
表达式是在编译阶段处理的。
第十三关: std::enable_if
和 SFINAE
上面我们已经有所提及,当我们希望根据某种条件来决定是否生成模板函数或类时,std::enable_if
非常有用。
例如,假设你有一个函数,你只希望当传入的类型是整数时,它才存在:
template <typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
functionOnlyForIntegers(T value) {
return value * 2;
}
第十四关:类模板偏特化
现在我们从头开始梳理一遍类模板。假设我们有以下基本模板。
template <typename T1, typename T2>
class MyPair {
T1 first;
T2 second;
// ... 其他成员函数 ...
};
接下来,对类模板偏特化。假设我们想为第二个模板参数是指针类型的所有情况提供特化。这里的"偏"意味着我们不是为两个特定的类型提供特化,而是只为一个类型(这里是 T2)提供。
template <typename T1, typename T2>
class MyPair<T1, T2*> {
T1 first;
T2* second;
// ... 其他成员函数 ...
};
需要注意的是,函数模板不支持偏特化,但可以通过重载来达到类似的效果。
第十五关:constexpr
和模板
constexpr
是 C++11 引入的关键字,它用于声明常量表达式,这些表达式在编译时就可以计算出结果。使用constexpr
与模板一起可以在编译时生成高效的代码。譬如下面的例子。
constexpr int factorial(int n) {
return (n <= 1) ? 1 : n * factorial(n - 1);
}
constexpr int value = factorial(5); // 120
那么结合 constexpr
和模板的例子是啥样的?当 constexpr
与模板结合使用时,你可以为各种类型创建编译时函数或实体,它们将针对给定的类型进行优化,并在编译时生成结果。
template <typename T>
constexpr T square(const T& value) {
return value * value;
}
constexpr int int_val = square(5); // 25
constexpr double double_val = square(5.0); // 25.0
两者结合的优势很大,我这里列出两点:
- 性能:结合
constexpr
和模板,生成的代码是在编译时优化的,这可以消除运行时计算的需要,从而提高性能。 - 泛型:模板使你可以为多种类型编写代码,而
constexpr
确保了对每种类型的高效实现。
第十六关:模板中的嵌套类型
一个模板可以在其内部定义另一个模板类:
template <typename T>
class Outer {
T data;
public:
template <typename U>
class Inner {
U innerData;
};
};
接下来,让我们给 Outer
和 Inner
类添加一些成员函数,使它们更具功能性。
template <typename T>
class Outer {
T data;
public:
Outer(T d) : data(d) {}
T getOuterData() const { return data; }
template <typename U>
class Inner {
U innerData;
public:
Inner(U d) : innerData(d) {}
U getInnerData() const { return innerData; }
};
};
使用示例:
Outer<int> outerInstance(10);
std::cout << "Outer data: " << outerInstance.getOuterData() << std::endl;
// Outputs: Outer data: 10
Outer<int>::Inner<double> innerInstance(5.5);
std::cout << "Inner data: " << innerInstance.getInnerData() << std::endl;
// Outputs: Inner data: 5.5
进一步添加功能,在 Outer
类中定义一个函数,该函数接受一个 Inner
对象并与之交互。
template <typename T>
class Outer {
T data;
public:
Outer(T d) : data(d) {}
T getOuterData() const { return data; }
// ---- template ----
template <typename U>
class Inner {
U innerData;
public:
Inner(U d) : innerData(d) {}
U getInnerData() const { return innerData; }
};
// ---- -------- ----
template <typename U>
void printCombinedData(const Inner<U>& inner) {
std::cout << "Combined data: " << data << " and " << inner.getInnerData() << std::endl;
}
};
// 使用:
Outer<int> outerInstance(10);
Outer<int>::Inner<double> innerInstance(5.5);
outerInstance.printCombinedData(innerInstance); // Outputs: Combined data: 10 and 5.5
总之需要知道,外部类完全可以访问其内部类及其成员,但它需要拥有内部类的对象实例才能访问内部类的非静态成员。
第十七关:模板参数包与展开
当使用变长模板参数时,你可以使用模板参数包。使用...
修饰的参数被称为参数包。
template <typename... Args>
void printValues(Args... args) {
(std::cout << ... << args); // 展开参数
}
int main() {
printValues(1, 2, 3, "hello", 'c');
//Same As : std::cout << 1 << 2 << 3 << "hello" << 'c';
}
如果要用多态来实现上面的效果,将会变得比较复杂。需要为每一种要输出的类型创建一个公共的基类并实现虚函数。然后为每种具体的类型实现一个子类。下面是用多态来实现的,可以看出模版参数包的优越性了吧。
#include <iostream>
#include <vector>
class Printable {
public:
virtual ~Printable() {}
virtual void print() const = 0;
};
class PrintInt : public Printable {
int value;
public:
PrintInt(int v) : value(v) {}
void print() const override {
std::cout << value;
}
};
class PrintString : public Printable {
std::string value;
public:
PrintString(const std::string& v) : value(v) {}
void print() const override {
std::cout << value;
}
};
class PrintChar : public Printable {
char value;
public:
PrintChar(char v) : value(v) {}
void print() const override {
std::cout << value;
}
};
void printValues(const std::vector<Printable*>& values) {
for (const auto& val : values) {
val->print();
}
}
int main() {
std::vector<Printable*> values = {new PrintInt(1), new PrintInt(2), new PrintInt(3), new PrintString("hello"), new PrintChar('c')};
printValues(values);
// Cleaning up
for (auto ptr : values) {
delete ptr;
}
}
还记得 十一关 讲解的折叠表达式吗?折叠表达式是 C++17 引入的,是一种新的、更简洁的方式来展开参数包,并对其应用特定的运算。在 C++17 之前,当需要在模板中使用参数包的时候,通常需要使用某种机制对其进行展开。在 C++11 和 C++14 中,展开参数包通常涉及到递归的模板技巧。例如,
template <typename T>
void printValues(T value) {
std::cout << value << std::endl;
}
template <typename First, typename... Rest>
void printValues(First first, Rest... rest) {
std::cout << first << ", ";
printValues(rest...); // 展开剩余的参数
}
// 使用
int main() {
printValues(1, 2, 3); // 输出: 1, 2, 3
printValues("a", "b", "c"); // 输出: a, b, c
return 0;
}
而使用了折叠表达式,就不用涉及递归输出了,上下两则代码完全一致。
template <typename... Args>
void printValues(Args... args) {
(std::cout << ... << args);
}
// 使用
int main() {
printValues(1, 2, 3, "hello", 'c'); // 输出:123helloc
return 0;
}
第十八关:Lambda 表达式与模板
auto lambda = []<typename T>(T value) { return value * 2; };
auto result = lambda(5); // result为10
进一步添加“概念”,以确保类型是可计算的。这里直接使用了std::is_arithmetic_v
。
#include <iostream>
#include <type_traits>
int main() {
auto genericLambda = [](auto x) {
static_assert(std::is_arithmetic_v<decltype(x)>, "Type must be arithmetic!");
return x * x;
};
std::cout << genericLambda(5) << std::endl; // 输出:25
std::cout << genericLambda(5.5) << std::endl; // 输出:30.25
// genericLambda("hello"); // 编译错误:Type must be arithmetic!
}
第十九关:模板递归
模板递归是一种非常强大的技巧,但也需要谨慎使用,因为它可能导致编译时间增加和代码膨胀。
在前面我们已经见识到了模版的强大。例如,计算阶乘或斐波那契数列,直接在编译期间就可以完成计算,减少运行时的计算量。
constexpr int factorial(int n) {
return (n <= 1) ? 1 : n * factorial(n - 1);
}
constexpr int value = factorial(5); // 120
第二十关:带有模板的继承
类模板可以继承自其他类模板。下面是一个最简单的例子,我们逐渐完善他。
template <typename T>
class Base {};
template <typename T>
class Derived : public Base<T> {};
1. 模版基类
可以创建一个模板基类,使得不同的子类可以以不同的方式特化或使用这个基类。
template <typename T>
class Base {
public:
T value;
Base(T val) : value(val) {}
void show() { std::cout << value << std::endl; }
};
class Derived : public Base<int> {
public:
Derived(int v) : Base(v) {}
void display() { std::cout << "Derived: " << value << std::endl; }
};
int main() {
Derived d(10);
d.show();
d.display();
}
2. 模版子类
可以使子类是模板,而基类不是。这样,就可以为基类定义一组行为,而子类则为这些行为提供具体的实现。
class Base {
public:
virtual void show() const = 0;
};
template <typename T>
class Derived : public Base {
T value;
public:
Derived(T v) : value(v) {}
void show() const override {
std::cout << "Value: " << value << std::endl;
}
};
int main() {
Derived<int> d1(5);
Derived<double> d2(3.14);
d1.show();
d2.show();
}
3. 在模板类中继承模板基类
子类和基类都可以是模板,这样你可以创建高度灵活和可重用的设计。
template <typename T>
class Base {
public:
T value;
Base(T val) : value(val) {}
virtual void show() const {
std::cout << "Base: " << value << std::endl;
}
};
template <typename T>
class Derived : public Base<T> {
public:
Derived(T v) : Base<T>(v) {}
void show() const override {
std::cout << "Derived: " << this->value << std::endl;
}
};
int main() {
Derived<int> d(10);
d.show();
}
第二十一关:std::type_trait
的工具集
<type_traits>
头文件提供了一组用于类型检查和修改的模板,可以在编译时获取和操作类型的信息。
static_assert(std::is_same<std::remove_const<const int>::type, int>::value);
以下是 std::type_traits
中一些常用的工具:
基础类型检查:
std::is_integral<T>
: 检查T是否是一个整数类型。std::is_floating_point<T>
: 检查T是否是一个浮点类型。std::is_arithmetic<T>
: 检查T是否是算术类型(整数或浮点数)。std::is_pointer<T>
: 检查T是否是指针。std::is_reference<T>
: 检查T是否是引用。std::is_array<T>
: 检查T是否是数组。std::is_enum<T>
: 检查T是否是枚举类型。
类型关系检查:
std::is_same<T, U>
: 检查两个类型是否完全相同。std::is_base_of<Base, Derived>
: 检查Base
是否是Derived
的基类。std::is_convertible<T, U>
: 检查类型T是否可以被隐式转换为U。
类型修改器:
std::remove_reference<T>
: 去除引用,得到裸类型。std::add_pointer<T>
: 为类型T添加一个指针。std::remove_pointer<T>
: 去除指针。std::remove_const<T>
: 去除常量限定符。std::add_const<T>
: 添加常量限定符。
其他:
std::underlying_type<T>
: 对于枚举类型T,得到对应的底层类型。std::result_of<F(Args...)>
: 对于函数类型F,返回它使用参数Args...
调用时的返回类型。
辅助类型:
- 对于上述的每个特性检查,都有一个对应的
_v
后缀的变量模板,如std::is_integral_v<T>
,它直接返回bool值,这使得代码更简洁。
- 对于上述的每个特性检查,都有一个对应的
static_assert(std::is_same<std::remove_const<const int>::type, int>::value);
static_assert(std::is_integral_v<int>);
第二十二关:模板与动态多态性
尽管模板提供了一种静态多态性形式,但它们也可以与虚函数和动态多态性结合使用。
#include <iostream>
#include <vector>
class Base {
public:
virtual void print() const {
std::cout << "Base class." << std::endl;
}
virtual ~Base() {}
};
class Derived1 : public Base {
public:
void print() const override {
std::cout << "Derived1 class." << std::endl;
}
};
class Derived2 : public Base {
public:
void print() const override {
std::cout << "Derived2 class." << std::endl;
}
};
template <typename T>
class Container {
private:
std::vector<T*> elements;
public:
void add(T* elem) {
elements.push_back(elem);
}
void printAll() const {
for (auto& elem : elements) {
elem->print();
}
}
};
int main() {
Container<Base> cont;
Derived1 d1;
Derived2 d2;
cont.add(&d1);
cont.add(&d2);
cont.printAll(); // Outputs: Derived1 class. Derived2 class.
return 0;
}