努力载入中...

C++模版小记:从入门到入土 - Remoooo

本文是专门为了我另一篇文章光栅化渲染器而写的。

第一关:为什么需要模版类?
第二关:「函数模版」
第三关:「类模版」
第四关:「多模板参数」与「非类型参数」
第五关:「模板特化」
第六关:「类型推断」
​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 的语法往往更为简洁和直观。

这里拓展一下,usingtypedef 两者一个主要的区别是,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;
}
  1. 模板声明:

    template <typename T>

    声明了一个模板函数,其中 T 是一个待定的类型。你可以为 T 提供任何类型,比如 intdoublestd::string 等,但是函数的实际行为取决于你提供的类型。

  2. 返回类型:

    typename std::enable_if<std::is_integral<T>::value>::type

    这段代码使用了两个主要的模板工具:std::enable_ifstd::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;
}

以下是一些常见的解释:

  1. 泛型编程: 使用模板,你可以为各种类型编写通用的代码,而不仅仅是那些你预先知道的类型。
  2. 类型约束: 通过 SFINAE 和其他模板技巧,你可以对哪些类型可以用于你的泛型代码施加更精细的约束。例如,你可能想要一个函数,它只接受具有某些成员函数的对象。
  3. 编译时优化: 由于模板在编译时实例化,编译器可以为每个特定的类型生成优化过的代码,这可能会导致更高的执行效率。
  4. 灵活性: 模板提供了更多的灵活性,例如模板元编程、模板特化等,允许更复杂和高效的编程技术。
  5. 类型透明性: 当使用模板时,原始类型信息在使用模板函数或类的地方保持不变。这与多态不同,其中类型信息可能会丢失,特别是在使用继承和虚函数时。

随着进一步学习以及项目的接触,我们可以更加体会到这种编程方式的优缺点。

第十关:模板与友元

模板类或函数可以声明为另一个类或函数的友元。

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;
    };
};

接下来,让我们给 OuterInner 类添加一些成员函数,使它们更具功能性。

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 中一些常用的工具:

  1. 基础类型检查:

    • 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是否是枚举类型。
  2. 类型关系检查:

    • std::is_same<T, U>: 检查两个类型是否完全相同。
    • std::is_base_of<Base, Derived>: 检查Base是否是Derived的基类。
    • std::is_convertible<T, U>: 检查类型T是否可以被隐式转换为U。
  3. 类型修改器:

    • std::remove_reference<T>: 去除引用,得到裸类型。
    • std::add_pointer<T>: 为类型T添加一个指针。
    • std::remove_pointer<T>: 去除指针。
    • std::remove_const<T>: 去除常量限定符。
    • std::add_const<T>: 添加常量限定符。
  4. 其他:

    • std::underlying_type<T>: 对于枚举类型T,得到对应的底层类型。
    • std::result_of<F(Args...)>: 对于函数类型F,返回它使用参数Args...调用时的返回类型。
  5. 辅助类型:

    • 对于上述的每个特性检查,都有一个对应的_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;
}

🔖

添加新评论

🚥
☕️
⬆️