C++ 模板基础知识——类模板、变量模板与别名模板

C++ 模板基础知识——类模板、变量模板与别名模板

1. 类模板的基本范例

类模板是用于生成类的模具。通过提供特定的模板参数,可以实例化出具体的类。这个概念与函数模板类似。

1.1 关键点
  1. 定义位置:类模板的声明和实现通常放在同一个头文件中,因为实例化时需要完整的模板信息。

  2. 语法

    template<typename T>
    class ClassName {
        // 类定义
    };
    
  3. 命名约定

    • ClassName: 类名或类模板名
    • ClassName<T>: 类型名(表示具体类型)
    • T: 模板参数(代表一个类型)
  4. 成员函数定义

    • 类内定义:直接在类模板内部定义
    • 类外定义:需要使用完整的类型名
      template<typename T>
      ReturnType ClassName<T>::FunctionName(Parameters) {
          // 函数实现
      }
      
  5. 实例化

    ClassName<SpecificType> objectName;
    
1.2 示例代码
#include <iostream>
#include <typeinfo>

// 类模板 MyVector,T 是模板参数,代表容器中元素的类型
template<typename T>
class MyVector {
public:
    // 定义迭代器类型,这里简单地使用指针作为迭代器
    typedef T* Iterator;

public:
    // 构造函数声明
    MyVector();
    
    // 赋值运算符重载声明
    // 注意:这里的 MyVector 可以写成 MyVector<T>,但在类内部可以省略 <T>
    MyVector& operator=(const MyVector&);

    // 成员函数,在类内直接定义
    void myFunc() {
        std::cout << "myFunc() called" << std::endl;
    }

public:
    Iterator begin(); // 返回指向容器首元素的迭代器
    Iterator end();   // 返回指向容器尾元素之后位置的迭代器
};

// 类外定义构造函数
template<typename T>
MyVector<T>::MyVector() {
    std::cout << "Type of T is: " << typeid(T).name() << std::endl;
}

int main() {
    
    MyVector<int> vec; // 实例化一个 int 类型的 MyVector   
    vec.myFunc(); // 调用 MyVector 的成员函数

    return 0;
}
1.3 注意事项
  1. 类模板中的成员函数只有在被调用时才会实例化。
  2. 在类模板外定义成员函数时,需要重复模板声明和使用完整的类型名。
  3. 模板参数可以有默认值,类似函数参数的默认值。
  4. 类模板可以有多个模板参数。
  5. 类模板可以特化,为特定类型提供专门的实现。

2. 类模板参数的推断

在C++ 17标准中,一个类模板的类型模板参数也变得可以推断了,编译器可以自动推断类模板的模板参数。这种推断主要发生在以下几种情况:

  1. 从构造函数参数推断
  2. 从初始化列表推断
  3. 从函数返回值推断
template<typename T>
class MyClass {
public:
    MyClass(T value) : m_value(value) 
    {
        std::cout << "Type of T is:" << typeid(T).name() << std::endl;
    }
private:
    T m_value;
};

// C++17之前
MyClass<int> obj1(42);

// C++17及以后,可以省略<int>
MyClass obj2(42);  // T 被推断为 int

3. 类模板的推断指南

推断指南(Deduction Guide)是C++17引入的一个重要特性,用于指导编译器如何从构造函数参数推断类模板的模板参数。这个特性主要解决了在某些情况下类模板参数无法自动推导的问题。

对于非聚合类模板,编译器会为每个构造函数自动生成一个隐式的推断指南。这意味着在大多数情况下,不需要显式地写推断指南。

template<typename T>
struct Container
{
    Container(T t)
    {
         std::cout << "Type of T is:" << typeid(t).name() << std::endl;
    }
};

int main()
{
    Container c1(15);       // 隐式推断为 Container<int>
    Container c2(12.8);     // 隐式推断为 Container<double>
    Container c3("Hello");  // 隐式推断为 Container<const char*>
    return 0;
}
3.1 自定义的推断指南

开发者可以编写自定义的推断指南,以处理一些特殊情况或覆盖默认行为。

自定义推断指南的优势:

  1. 实现更智能的类型推断
  2. 简化复杂模板的使用
  3. 提供更直观的 API
  4. 在不修改原有类模板的情况下调整类型推断行为

自定义推断指南的一般语法是:

template<parameter-list>
ClassName(constructor-params) -> ClassName<deduced-types>;
  • template<parameter-list>:这部分是可选的。如果需要使用模板参数,就在这里声明。
  • ClassName(constructor-params):这部分模仿构造函数的参数列表。
  • ->:箭头表示"推断为"。
  • ClassName<deduced-types>:这是推断的结果,指定了应该使用的模板参数。

实例:

template<typename T>
struct Container 
{
    Container(T t)
    {
         std::cout << "Type of T is:" << typeid(t).name() << std::endl;
    }
    Container(const char* c)
    {
        std::cout << "Type of T is:" << typeid(c).name() << std::endl;
    }
};
// 自定义推断指南
Container(const char*) -> Container<std::string>;
int main() 
{
    Container c1(42);        // Container<int>
    Container c2("Hello");   // Container<std::string>,而不是 Container<const char*>
}
3.2 自定义推断指南使用场景
  • 从一种类型推断出相关但不同的类型
  • 类模板有多个模板参数,但仅需从部分参数推断类型
  • 为特定的构造函数参数组合指定特定的模板参数
1. 从一种类型推断出相关但不同的类型

这种情况适用于需要进行类型转换或使用相关类型的场景。

示例:从原始指针类型推断智能指针类型

template<typename T>
class SmartPtr {
public:
    SmartPtr(T* ptr) : m_ptr(ptr) {}
private:
    T* m_ptr;
};

// 推断指南
template<typename T>
SmartPtr(T*) -> SmartPtr<T>;

// 使用
int* raw_ptr = new int(42);
SmartPtr smart_ptr = raw_ptr;  // SmartPtr<int>,而非 SmartPtr<int*>

此例中,推断指南将原始指针类型 T* 转换为 SmartPtr<T>,而非 SmartPtr<T*>

2. 类模板有多个模板参数,但仅需从部分参数推断类型

这种情况常见于复杂模板类,其中部分参数需要推断,而其他参数使用默认值或手动指定。

示例:部分参数推断的键值存储

template<typename Key, typename Value, typename Compare = std::less<Key>>
class KeyValueStore {
public:
    KeyValueStore(std::initializer_list<std::pair<const Key, Value>> init) {}
};

// 推断指南:仅从初始化列表推断 Key 和 Value,使用默认的 Compare
template<typename Key, typename Value>
KeyValueStore(std::initializer_list<std::pair<const Key, Value>>) 
    -> KeyValueStore<Key, Value>;

// 使用
KeyValueStore store = {{"apple", 1}, {"banana", 2}};  // KeyValueStore<const char*, int>

此例中,推断指南仅从初始化列表推断 KeyValue 类型,而 Compare 使用默认值。

3. 为特定的构造函数参数组合指定特定的模板参数

这种情况允许为不同的构造函数参数组合指定不同的模板参数推断规则。

示例:根据构造函数参数选择不同的内部存储类型

#include <vector>
#include <list>

template<typename T>
class Container {
public:
    Container(std::initializer_list<T> init) : vec(init) {}
    Container(size_t size, T value) : list(size, value) {}

private:
    std::vector<T> vec;
    std::list<T> list;
};

// 推断指南:使用初始化列表时,推断为 vector 存储
template<typename T>
Container(std::initializer_list<T>) -> Container<T>;

// 推断指南:使用 size 和 value 时,推断为 list 存储
template<typename T>
Container(size_t, T) -> Container<T>;

// 使用
Container c1 = {1, 2, 3};  // 使用 vector 存储
Container c2(5, 10);       // 使用 list 存储

4. 类模板的特化

类模板特化是一种允许程序员为特定类型提供专门实现的技术。它在需要针对某些类型进行优化或提供特殊行为时非常有用。

4.1 类模板的全特化

全特化是为类模板的所有模板参数提供具体类型的特殊实现。

语法:

template<>
class ClassName<SpecificType1, SpecificType2, ...> {
    // 特化实现
};

示例:

#include <iostream>

// 主模板
template<typename T>
class DataHandler {
public:
    void process(T data) {
        std::cout << "Processing generic data" << std::endl;
    }
};

// 全特化版本 for int
template<>
class DataHandler<int> {
public:
    void process(int data) {
        std::cout << "Processing integer data: " << data << std::endl;
    }
};

int main() {
    DataHandler<double> doubleHandler;
    DataHandler<int> intHandler;

    doubleHandler.process(3.14);  // 输出: Processing generic data
    intHandler.process(42);       // 输出: Processing integer data: 42

    return 0;
}

注意事项:

  • 全特化必须在原始模板之后定义。
  • 全特化版本可以有完全不同的实现,包括不同的成员函数和数据成员。
4.2 普通成员函数的全特化

可以只特化类模板的特定成员函数,而不是整个类。

语法:

template<>
ReturnType ClassName<SpecificType>::FunctionName(Parameters) {
    // 特化实现
}

示例:

#include <iostream>
#include <type_traits>

template<typename T>
class MathOperations {
public:
    T square(T value);
};

// 通用实现
template<typename T>
T MathOperations<T>::square(T value) {
    return value * value;
}

// 特化 square 函数 for bool
template<>
bool MathOperations<bool>::square(bool value) {
    return value; // 对于 bool,square 操作等同于身份操作
}

int main() {
    MathOperations<int> intMath;
    MathOperations<bool> boolMath;

    std::cout << intMath.square(4) << std::endl;   // 输出: 16
    std::cout << boolMath.square(true) << std::endl; // 输出: 1

    return 0;
}

注意事项:

  • 成员函数特化必须在类模板定义之外进行。
  • 只有已经在主模板中声明的成员函数才能被特化。
4.3 静态成员变量的全特化

静态成员变量也可以被特化,为特定类型提供不同的值。

语法:

template<>
Type ClassName<SpecificType>::StaticMemberName = value;

示例:

#include <iostream>

template<typename T>
class TypeProperties {
public:
    static const char* name;
    static const bool is_integral;
};

// 通用实现
template<typename T>
const char* TypeProperties<T>::name = "Unknown";

template<typename T>
const bool TypeProperties<T>::is_integral = false;

// 特化 for int
template<>
const char* TypeProperties<int>::name = "Integer";

template<>
const bool TypeProperties<int>::is_integral = true;

int main() {
    std::cout << TypeProperties<int>::name << ", "
              << TypeProperties<int>::is_integral << std::endl;
    // 输出: Integer, 1

    std::cout << TypeProperties<double>::name << ", "
              << TypeProperties<double>::is_integral << std::endl;
    // 输出: Unknown, 0

    return 0;
}

注意事项:

  • 静态成员变量的特化必须在全局命名空间中定义。
  • 特化的静态成员变量必须有一个显式的初始化器。
4.4 类模板的偏特化(局部特化)

偏特化允许为一部分模板参数提供特殊实现,而其他参数保持泛型。

语法:

template<typename T1, typename T2, ...>
class ClassName<SpecificType, T2, ...> {
    // 偏特化实现
};

示例:

#include <iostream>

// 主模板
template<typename T, typename U>
class DataPair {
public:
    void printTypes() {
        std::cout << "General template" << std::endl;
    }
};

// 偏特化:第一个类型为 int
template<typename U>
class DataPair<int, U> {
public:
    void printTypes() {
        std::cout << "Partial specialization: int and generic type" << std::endl;
    }
};

// 偏特化:两个类型相同
template<typename T>
class DataPair<T, T> {
public:
    void printTypes() {
        std::cout << "Partial specialization: same types" << std::endl;
    }
};

int main() {
    DataPair<double, char> p1;
    DataPair<int, double> p2;
    DataPair<int, int> p3;

    p1.printTypes(); // 输出: General template
    p2.printTypes(); // 输出: Partial specialization: int and generic type
    p3.printTypes(); // 输出: Partial specialization: same types

    return 0;
}
4.5 注意事项
  • 偏特化必须至少特化一个模板参数。
  • 偏特化可以引入新的模板参数。
  • 编译器会选择最特殊的匹配版本。

5. 默认参数

类模板的默认参数允许我们在使用模板时省略某些类型参数,从而简化模板的使用。

5.1 常规默认参数

规则:
如果某个模板参数有默认值,那么从这个参数开始,后面的所有模板参数都必须有默认值。

语法:

template<typename T1, typename T2 = DefaultType2, typename T3 = DefaultType3>
class ClassName {
    // 类定义
};

示例:

#include <iostream>
#include <vector>
#include <deque>

template<typename T, typename Container = std::vector<T>>
class Stack {
private:
    Container elements;

public:
    void push(const T& elem) {
        elements.push_back(elem);
    }

    T pop() {
        if (elements.empty()) {
            throw std::out_of_range("Stack is empty");
        }
        T top = elements.back();
        elements.pop_back();
        return top;
    }
};

int main() {
    Stack<int> intStack;           // 使用默认的 std::vector<int>
    Stack<double, std::deque<double>> doubleStack; // 显式指定容器类型

    intStack.push(10);
    doubleStack.push(3.14);

    std::cout << intStack.pop() << std::endl;     // 输出: 10
    std::cout << doubleStack.pop() << std::endl;  // 输出: 3.14

    return 0;
}
5.2 后面的模板参数依赖前面的模板参数

模板参数可以依赖于之前定义的参数。这允许更复杂和灵活的默认值设置。

示例:

#include <iostream>
#include <type_traits>

template<typename T, typename U = typename std::make_unsigned<T>::type>
class NumberConverter {
public:
    U convert(T value) {
        return static_cast<U>(value);
    }
};

int main() {
    NumberConverter<int> converter;
    std::cout << converter.convert(-1) << std::endl;  // 输出: 4294967295 (2^32 - 1)

    NumberConverter<int, long> customConverter;
    std::cout << customConverter.convert(-1) << std::endl;  // 输出: -1

    return 0;
}

在这个例子中,第二个模板参数 U 默认为 T 的无符号版本。

5.3 在模板声明中指定默认参数

默认参数可以在模板的前向声明中指定,但在定义中不需要重复。

语法:

// 前向声明
template<typename T, typename U = int>
class MyClass;

// 定义
template<typename T, typename U>
class MyClass {
    // 类定义
};

示例:

#include <iostream>

// 前向声明with默认参数
template<typename T, typename U = double>
class Pair;

// 定义不需要重复默认参数
template<typename T, typename U>
class Pair {
public:
    T first;
    U second;

    Pair(T f, U s) : first(f), second(s) {}

    void print() {
        std::cout << "First: " << first << ", Second: " << second << std::endl;
    }
};

int main() {
    Pair<int> p1(1, 3.14);  // U 默认为 double
    Pair<std::string, int> p2("Hello", 42);

    p1.print();  // 输出: First: 1, Second: 3.14
    p2.print();  // 输出: First: Hello, Second: 42

    return 0;
}
5.4 注意事项
  1. 默认参数可以是内置类型、类类型,甚至是其他模板类型。
  2. 默认参数可以使模板更易于使用,但也可能导致代码复杂性增加。
  3. 在使用默认参数时,要注意可能的性能影响和类型安全问题。
  4. 如果在类模板声明中指定了默认参数,在定义中就不能再次指定。

6. 类型别名

在模板编程中,类型名称往往会变得冗长复杂,特别是在实际项目中。为了简化代码并提高可读性,C++提供了两种创建类型别名的方法:typedefusing

6.1 使用 typedef

typedef 是传统的创建类型别名的方法,在C++98及之后的所有标准中都支持。

语法:

typedef 原始类型名 别名;

示例:

#include <iostream>
#include <vector>
#include <map>

template<typename T, typename U>
class ComplexDataStructure {
    // 类定义
};

typedef ComplexDataStructure<int, float> IntFloatCDS;
typedef std::vector<std::map<int, std::string>> ComplexVector;

int main() {
    IntFloatCDS data1;
    ComplexVector data2;

    // 使用这些别名可以大大简化代码
    return 0;
}
6.2 使用 using(C++11及以后)

using 是C++11引入的新方法,它提供了更清晰和一致的语法,特别是在处理模板时。

语法:

using 别名 = 原始类型名;

示例:

#include <iostream>
#include <vector>
#include <map>

template<typename T, typename U>
class ComplexDataStructure {
    // 类定义
};

using IntFloatCDS = ComplexDataStructure<int, float>;
using ComplexVector = std::vector<std::map<int, std::string>>;

int main() {
    IntFloatCDS data1;
    ComplexVector data2;

    // 使用这些别名可以大大简化代码
    return 0;
}
6.3 typedef 和 using 的比较
  1. 可读性: using 通常被认为更易读,特别是对于复杂的模板类型。
  2. 一致性: using 的语法更一致,特别是在处理模板别名时。
  3. 功能: using 可以用于模板别名(template alias),而 typedef 不能。
  4. 兼容性: typedef 在所有C++版本中都支持,而 using 需要C++11或更高版本。
6.4 在模板中使用类型别名

类型别名在模板内部特别有用,可以简化内部类型的表示。

示例:

#include <iostream>
#include <vector>

template<typename T>
class DataContainer {
public:
    using ValueType = T;
    using Container = std::vector<T>;
    using Iterator = typename Container::iterator;

private:
    Container data;

public:
    void add(const ValueType& value) {
        data.push_back(value);
    }

    Iterator begin() { return data.begin(); }
    Iterator end() { return data.end(); }
};

int main() {
    DataContainer<int> intContainer;
    intContainer.add(10);
    intContainer.add(20);

    for (DataContainer<int>::Iterator it = intContainer.begin(); it != intContainer.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    return 0;
}

7. 非类型模板参数

非类型模板参数是在编译时就确定的常量值,而不是类型。这些参数可以用于创建更加灵活和高效的模板。

7.1 基本概念

非类型模板参数可以是:

  • 整型常量(包括枚举)
  • 指针(包括函数指针和成员函数指针)
  • 左值引用
  • std::nullptr_t(从C++11开始)
  • 浮点型和类类型(从C++20开始)

语法:

template<typename T, 非类型参数>
class ClassName {
    // 类定义
};
7.2 整型非类型参数

整型非类型参数是最常见的形式,通常用于指定数组大小或其他编译时常量。

示例:

#include <iostream>
#include <array>

template<typename T, std::size_t Size>
class FixedArray {
private:
    std::array<T, Size> data;

public:
    T& operator[](std::size_t index) {
        return data[index];
    }

    const T& operator[](std::size_t index) const {
        return data[index];
    }

    std::size_t size() const {
        return Size;
    }
};

int main() {
    FixedArray<int, 5> intArray;
    for (std::size_t i = 0; i < intArray.size(); ++i) {
        intArray[i] = i * 10;
    }

    for (std::size_t i = 0; i < intArray.size(); ++i) {
        std::cout << intArray[i] << " ";
    }
    std::cout << std::endl;

    return 0;
}
7.3 指针非类型参数

指针非类型参数可以用于传递函数指针或对象指针。

示例:

#include <iostream>

template<typename T, T* DefaultValue>
class OptionalValue {
private:
    T value;
    bool hasValue;

public:
    OptionalValue() : value(*DefaultValue), hasValue(false) {}
    OptionalValue(const T& v) : value(v), hasValue(true) {}

    T getValue() const {
        return hasValue ? value : *DefaultValue;
    }
};

int defaultInt = 0;
double defaultDouble = 0.0;

int main() {
    OptionalValue<int, &defaultInt> intOpt;
    OptionalValue<int, &defaultInt> intOpt2(42);
    OptionalValue<double, &defaultDouble> doubleOpt(3.14);

    std::cout << intOpt.getValue() << std::endl;    // 输出: 0
    std::cout << intOpt2.getValue() << std::endl;   // 输出: 42
    std::cout << doubleOpt.getValue() << std::endl; // 输出: 3.14

    return 0;
}
7.4 函数指针作为非类型参数

函数指针可以用作非类型模板参数,允许在编译时选择不同的函数实现。

示例:

#include <iostream>

template<typename T, T (*Operation)(T, T)>
class MathOperation {
public:
    T execute(T a, T b) {
        return Operation(a, b);
    }
};

int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }

int main() {
    MathOperation<int, add> adder;
    MathOperation<int, multiply> multiplier;

    std::cout << "Add: " << adder.execute(5, 3) << std::endl;        // 输出: 8
    std::cout << "Multiply: " << multiplier.execute(5, 3) << std::endl; // 输出: 15

    return 0;
}
7.5 C++20中的浮点型和类类型非类型参数

从C++20开始,浮点型和某些类类型也可以作为非类型模板参数。

示例:

#include <iostream>

template<double Value>
class ConstantMultiplier {
public:
    double multiply(double input) {
        return input * Value;
    }
};

int main() {
    ConstantMultiplier<3.14> piMultiplier;
    std::cout << piMultiplier.multiply(2) << std::endl;  // 输出: 6.28

    return 0;
}
7.6 注意事项
  1. 非类型模板参数必须是常量表达式,在编译时就能确定。
  2. 使用非类型模板参数可以提高运行时效率,因为某些计算可以在编译时完成。
  3. 非类型模板参数可以与类型模板参数结合使用,创建更灵活的模板。
  4. 对于大型项目,过度使用非类型模板参数可能会增加编译时间和二进制文件大小。
  5. 在C++17之前,非类型模板参数不能是浮点数或类对象。
  6. 使用非类型模板参数时要注意类型匹配,例如 intstd::size_t 可能在不同平台上有不同的大小。

8. 成员函数模板

8.1 基本含义、构造函数模板

成员函数模板是类中的模板化成员函数。它们允许类的成员函数接受不同类型的参数,从而增加类的灵活性。

基本语法:

class MyClass {
public:
    template<typename T>
    void functionName(T parameter) {
        // 函数实现
    }
};

构造函数模板示例:

#include <iostream>
#include <vector>

class Container {
private:
    std::vector<int> data;

public:
    template<typename Iter>
    Container(Iter begin, Iter end) : data(begin, end) {}

    void print() const {
        for (int val : data) {
            std::cout << val << " ";
        }
        std::cout << std::endl;
    }
};

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    Container c1(vec.begin(), vec.end());
    c1.print();  // 输出: 1 2 3 4 5

    int arr[] = {6, 7, 8, 9, 10};
    Container c2(std::begin(arr), std::end(arr));
    c2.print();  // 输出: 6 7 8 9 10

    return 0;
}
8.2 拷贝构造函数模板与拷贝赋值运算符模板

拷贝构造函数和拷贝赋值运算符也可以是模板,这允许从不同类型的对象进行复制或赋值。

示例:

#include <iostream>
#include <vector>

template<typename T>
class MyContainer {
private:
    std::vector<T> data;

public:
    MyContainer() = default;

    // 拷贝构造函数模板
    template<typename U>
    MyContainer(const MyContainer<U>& other) {
        data.clear();
        for (const auto& item : other.getData()) {
            data.push_back(static_cast<T>(item));
        }
    }

    // 拷贝赋值运算符模板
    template<typename U>
    MyContainer& operator=(const MyContainer<U>& other) {
        if (static_cast<void*>(this) != static_cast<const void*>(&other)) {
            data.clear();
            for (const auto& item : other.getData()) {
                data.push_back(static_cast<T>(item));
            }
        }
        return *this;
    }

    void add(const T& value) {
        data.push_back(value);
    }

    const std::vector<T>& getData() const {
        return data;
    }

    void print() const {
        for (const auto& item : data) {
            std::cout << item << " ";
        }
        std::cout << std::endl;
    }
};

int main() {
    MyContainer<int> intContainer;
    intContainer.add(1);
    intContainer.add(2);
    intContainer.add(3);

    MyContainer<double> doubleContainer = intContainer;  // 使用拷贝构造函数模板
    doubleContainer.print();  // 输出: 1 2 3

    MyContainer<float> floatContainer;
    floatContainer = intContainer;  // 使用拷贝赋值运算符模板
    floatContainer.print();  // 输出: 1 2 3

    return 0;
}
8.3 特化

成员函数模板也可以进行特化,为特定类型提供专门的实现。

示例:

#include <iostream>
#include <string>

class StringConverter {
public:
    template<typename T>
    std::string convert(T value) {
        return std::to_string(value);
    }
};

// 特化版本
template<>
std::string StringConverter::convert<const char*>(const char* value) {
    return std::string(value);
}

int main() {
    StringConverter converter;

    std::cout << converter.convert(42) << std::endl;        // 输出: 42
    std::cout << converter.convert(3.14) << std::endl;      // 输出: 3.140000
    std::cout << converter.convert("Hello") << std::endl;   // 输出: Hello

    return 0;
}
8.4 注意事项
  1. 成员函数模板可以与普通成员函数共存。
  2. 成员函数模板不能是虚函数。
  3. 成员函数模板可以提高代码的复用性和灵活性。
  4. 使用成员函数模板时,要注意类型安全和隐式类型转换可能带来的问题。
  5. 对于需要高度通用性的类,成员函数模板是一个强大的工具。
  6. 在使用拷贝构造函数模板和拷贝赋值运算符模板时,要注意处理自赋值和异常安全。
  7. 特化可以用于为特定类型提供更高效或更专门化的实现。

9. 类/类模板中的类模板(类模板的嵌套)

类模板的嵌套是指在一个类或类模板内部定义另一个类模板。这种技术可以创建复杂的数据结构和灵活的设计模式。

9.1 基本语法
template <typename T>
class OuterClass {
public:
    template <typename U>
    class InnerClass {
        // 内部类模板的定义
    };
    
    // 外部类的其他成员
};
9.2 示例和用法

示例:

#include <iostream>
#include <vector>

// 外部类模板 Container
template <typename T>
class Container {
public:
    // 内部类模板 Iterator
    template <typename U>
    class Iterator {
    private:
        typename std::vector<T>::iterator current;

    public:
        Iterator(typename std::vector<T>::iterator it) : current(it) {}

        // 解引用操作符,返回当前元素
        U operator*() const {
            return static_cast<U>(*current);
        }

        // 前缀递增操作符
        Iterator& operator++() {
            ++current;
            return *this;
        }

        // 不等于操作符,用于比较两个迭代器
        bool operator!=(const Iterator& other) const {
            return current != other.current;
        }
    };

private:
    std::vector<T> data; // 存储数据的容器

public:
    // 添加元素到容器
    void add(const T& item) {
        data.push_back(item);
    }

    // 返回指向容器开始的迭代器
    Iterator<T> begin() {
        return Iterator<T>(data.begin());
    }

    // 返回指向容器结束的迭代器
    Iterator<T> end() {
        return Iterator<T>(data.end());
    }
};

int main() {
    // 使用 int 类型实例化 Container
    Container<int> intContainer;
    intContainer.add(1);
    intContainer.add(2);
    intContainer.add(3);

    // 使用迭代器遍历并打印 int 容器的内容
    std::cout << "Int Container: ";
    for (auto it = intContainer.begin(); it != intContainer.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    // 使用 double 类型实例化 Container
    Container<double> doubleContainer;
    doubleContainer.add(1.1);
    doubleContainer.add(2.2);
    doubleContainer.add(3.3);

    // 使用迭代器遍历并打印 double 容器的内容
    std::cout << "Double Container: ";
    for (auto it = doubleContainer.begin(); it != doubleContainer.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    return 0;
}

在这个例子中:

  1. Container 是外部类模板,它有一个类型参数 T
  2. Iterator 是内部类模板,嵌套在 Container 内部,它有自己的类型参数 U
  3. Iterator 类提供了基本的迭代器功能,允许遍历 Container 中的元素。
  4. 主函数演示了如何使用这个嵌套的类模板结构。
9.3 优点和应用
  1. 封装性:内部类模板可以访问外部类的私有成员,这提供了更好的封装。
  2. 灵活性:允许创建与外部类紧密相关但又独立可配置的组件。
  3. 类型安全:可以在编译时确保内部类与外部类类型的兼容性。
  4. 代码组织:有助于将相关的功能组织在一起,提高代码的可读性和维护性。
9.4 注意事项
  1. 命名空间考虑:使用嵌套类模板时,需要注意完整的类型名称,例如 Container<int>::Iterator<int>
  2. 模板参数:内部类模板可以使用外部类的模板参数,也可以有自己的模板参数。
  3. 实例化:嵌套的类模板只有在被使用时才会实例化。
  4. 复杂性:过度使用嵌套类模板可能导致代码复杂度增加,应适度使用。
9.5 高级应用

嵌套类模板常用于实现:

  1. 自定义容器和迭代器。
  2. 复杂的设计模式,如策略模式或观察者模式。
  3. 元编程技术,如类型特性或策略类。

10. 变量模板与成员变量模板

变量模板是C++14引入的特性,允许创建可根据不同类型生成不同值的变量。这一特性增强了C++的泛型编程能力,使代码更加灵活和可重用。

10.1 变量模板的特化

变量模板的核心思想是创建适用于多种类型的变量。

示例:使用变量模板表示圆周率(π)

// 基本变量模板
template<typename T>
constexpr T pi = T(3.1415926535897932385);

// 特化版本
template<>
constexpr const char* pi<const char*> = "3.14159";

// 使用
std::cout << pi<double> << std::endl;  // 输出: 3.14159265358979
std::cout << pi<float> << std::endl;   // 输出: 3.14159
std::cout << pi<const char*> << std::endl;  // 输出: 3.14159
10.2 默认模板参数

默认模板参数可以在不指定类型的情况下使用变量模板:

template<typename T = double>
constexpr T e = T(2.7182818284590452353);

std::cout << e<> << std::endl;  // 使用默认类型 double
std::cout << e<float> << std::endl;  // 显式指定 float
10.3 非类型模板参数

非类型模板参数允许使用具体的值作为模板参数,而不仅仅是类型:

template<int N>
constexpr double pow_of_2 = 1 << N;

std::cout << pow_of_2<3> << std::endl;  // 2^3 = 8
std::cout << pow_of_2<5> << std::endl;  // // 2^5 = 32
10.4 变量模板的另一种形式

这种形式基于现有类模板,为其静态成员创建更简便的访问方式。这种形式简化了对类模板静态成员的访问,并允许独立修改变量模板实例的值。

template<typename T>
struct B {
    const static T value = 160;
};

template<typename T>
int g_myvar4 = B<T>::value;

std::cout << g_myvar4<int> << std::endl;  // 输出: 160
g_myvar4<int> = 152;  // 可以修改变量模板实例
std::cout << g_myvar4<int> << std::endl;  // 输出: 152
std::cout << B<int>::value << std::endl;  // 输出: 160(原值不变)
10.5 成员变量模板

类中也可以定义成员变量模板,成员变量模板允许在单个类中为不同类型定义模板化的成员变量。

class MyClass {
public:
    template<typename T>
    static constexpr T value = T(100);

    template<typename T>
    T data;
};

// 使用
std::cout << MyClass::value<int> << std::endl;  // 输出: 100
std::cout << MyClass::value<double> << std::endl;  // 输出: 100

MyClass obj;
obj.data<int> = 42;
obj.data<std::string> = "Hello";
std::cout << obj.data<int> << std::endl;  // 输出: 42
std::cout << obj.data<std::string> << std::endl;  // 输出: Hello

综合示例:

#include <iostream>
#include <string>
#include <type_traits>

// 1. 基本变量模板
template<typename T>
constexpr T pi = T(3.1415926535897932385);

// 2. 变量模板的特化
template<>
constexpr const char* pi<const char*> = "3.14159";

// 3. 带默认参数的变量模板
template<typename T = double>
constexpr T e = T(2.7182818284590452353);

// 4. 非类型模板参数
template<int N>
constexpr double pow_of_2 = 1 << N;

// 5. 变量模板的另一种形式
template<typename T>
struct B {
    const static T value = 160;
};

template<typename T>
int g_myvar4 = B<T>::value;

// 6. 包含静态成员变量模板的类
class MathConstants {
public:
    template<typename T>
    static constexpr T golden_ratio = T(1.6180339887498948);

    // 非模板成员变量
    int regular_constant;

    // 成员函数模板(这是允许的)
    template<typename T>
    T get_user_defined_constant() {
        return T(regular_constant);
    }
};

// 7. 使用 type_traits 的变量模板
template<typename T>
constexpr bool is_numeric = std::is_arithmetic<T>::value;

int main() {
    // 1. 使用基本变量模板
    std::cout << "Pi (double): " << pi<double> << std::endl;
    std::cout << "Pi (float): " << pi<float> << std::endl;

    // 2. 使用特化的变量模板
    std::cout << "Pi (const char*): " << pi<const char*> << std::endl;

    // 3. 使用带默认参数的变量模板
    std::cout << "e (default double): " << e<> << std::endl;
    std::cout << "e (float): " << e<float> << std::endl;

    // 4. 使用非类型模板参数
    std::cout << "2^3: " << pow_of_2<3> << std::endl;
    std::cout << "2^5: " << pow_of_2<5> << std::endl;

    // 5. 使用变量模板的另一种形式
    std::cout << "Original B<int>::value: " << B<int>::value << std::endl;
    std::cout << "Original g_myvar4<int>: " << g_myvar4<int> << std::endl;
    g_myvar4<int> = 152;
    std::cout << "Modified g_myvar4<int>: " << g_myvar4<int> << std::endl;
    std::cout << "B<int>::value after modification: " << B<int>::value << std::endl;

    // 6. 使用静态成员变量模板
    std::cout << "Golden ratio (float): " << MathConstants::golden_ratio<float> << std::endl;
    std::cout << "Golden ratio (double): " << MathConstants::golden_ratio<double> << std::endl;

    MathConstants obj;
    obj.regular_constant = 42;
    std::cout << "User defined (int): " << obj.get_user_defined_constant<int>() << std::endl;
    std::cout << "User defined (double): " << obj.get_user_defined_constant<double>() << std::endl;

    // 7. 使用 type_traits 的变量模板
    std::cout << "Is int numeric? " << is_numeric<int> << std::endl;
    std::cout << "Is double numeric? " << is_numeric<double> << std::endl;
    std::cout << "Is string numeric? " << is_numeric<std::string> << std::endl;

    return 0;
}

这个综合示例包含了以下内容:

  1. 基本变量模板(pi
  2. 变量模板的特化(pi<const char*>
  3. 带默认参数的变量模板(e
  4. 使用非类型模板参数的变量模板(pow_of_2
  5. 变量模板的另一种形式,基于类模板的静态成员(Bg_myvar4
  6. 包含成员变量模板的类(MathConstants
  7. 使用 type_traits 的变量模板(is_numeric

11. 别名模板与成员别名模板

11.1 别名模板(Alias Templates)
  1. 定义

    • C++11 引入的特性
    • 使用 using 关键字创建模板化的类型别名
  2. 基本语法

    template<typename T>
    using AliasName = OriginalType<T>;
    
  3. 示例

    template<typename T>
    using str_map_t = std::map<std::string, T>;
    
    str_map_t<int> map1;
    map1.insert({"first", 1});
    map1.insert({"second", 2});
    
  4. 优点

    • 简化复杂类型的书写
    • 提高代码可读性
    • 在模板编程中特别有用
11.2 成员别名模板
  1. 定义

    • 在类或类模板内部定义的别名模板
  2. 语法

    template<typename T>
    class MyClass {
    public:
        template<typename U>
        using InnerAlias = SomeType<T, U>;
    };
    
  3. 示例

    template<typename T>
    class E {
    public:
        template<typename U>
        using str_map_t = std::map<std::string, U>;
    
        str_map_t<int> map1;
    };
    
    E<float> obja;
    obja.map1.insert({"first", 1});
    obja.map1.insert({"second", 2});
    
  4. 特点

    • 可以在类的顶部定义,不需要访问修饰符
    • 可以使用类模板的类型参数

12. 模板模板参数

模板模板参数是C++中一个高级但非常强大的特性,它允许将模板本身作为另一个模板的参数。

12.1 基本概念
  1. 模板模板参数是一个模板作为另一个模板的参数。

  2. 允许在不指定具体类型的情况下,使用容器或其他模板作为参数。

  3. 语法

    template <template <typename...> class Container>
    class MyClass {
        // ...
    };
    
12.2 基本示例
#include <iostream>
#include <vector>
#include <list>

template <template <typename, typename> class Container, typename T>
class Wrapper {
public:
    Container<T, std::allocator<T>> data;

    void add(const T& value) {
        data.push_back(value);
    }

    void print() {
        for (const auto& item : data) {
            std::cout << item << " ";
        }
        std::cout << std::endl;
    }
};

int main() {
    Wrapper<std::vector, int> vecWrapper;
    vecWrapper.add(1);
    vecWrapper.add(2);
    vecWrapper.add(3);
    vecWrapper.print();

    Wrapper<std::list, double> listWrapper;
    listWrapper.add(1.1);
    listWrapper.add(2.2);
    listWrapper.add(3.3);
    listWrapper.print();

    return 0;
}
12.3 模板模板参数的匹配规则
  1. 模板模板参数必须精确匹配,包括模板参数的数量和类型。
  2. C++17之前,默认模板参数不参与匹配。
  3. C++17引入了更灵活的匹配规则,允许部分匹配。
12.4 使用 typename 或 class

在C++17之前,模板模板参数只能使用 class 关键字:

template <template <typename, typename> class Container>

C++17之后,也可以使用 typename

template <template <typename, typename> typename Container>
12.5 变参模板模板参数

可以使用变参模板来创建更灵活的模板模板参数:

template <template <typename...> class Container, typename... Args>
class FlexibleWrapper {
    Container<Args...> data;
    // ...
};
12.6 实际应用示例
#include <iostream>
#include <vector>
#include <deque>
#include <list>

template <template <typename...> typename Container, typename T>
class DataStructure {
private:
    Container<T> data;

public:
    void add(const T& value) {
        data.push_back(value);
    }

    void print() const {
        for (const auto& item : data) {
            std::cout << item << " ";
        }
        std::cout << std::endl;
    }
};

int main() {
    DataStructure<std::vector, int> vec_data;
    vec_data.add(1);
    vec_data.add(2);
    vec_data.add(3);
    std::cout << "Vector: ";
    vec_data.print();

    DataStructure<std::deque, double> deque_data;
    deque_data.add(1.1);
    deque_data.add(2.2);
    deque_data.add(3.3);
    std::cout << "Deque: ";
    deque_data.print();

    DataStructure<std::list, std::string> list_data;
    list_data.add("Hello");
    list_data.add("World");
    std::cout << "List: ";
    list_data.print();

    return 0;
}
12.7 注意事项
  1. 模板模板参数可能导致代码复杂性增加,应谨慎使用。
  2. 在C++17之前,使用模板模板参数可能会遇到一些匹配问题,特别是涉及默认模板参数时。
  3. 模板模板参数通常用于高级泛型编程和库设计中。

13. 共用体模板(联合模板)

13.1 基本概念
  • 共用体(union)是一种特殊的类,允许在同一内存位置存储不同的数据类型。
  • 共用体模板是将模板概念应用于共用体,使其能够处理不同类型的数据。
13.2 语法
template <typename T1, typename T2>
union UnionTemplate {
    T1 value1;
    T2 value2;
};
13.3 基本示例
#include <iostream>

template <typename T1, typename T2>
union UnionTemplate {
    T1 value1;
    T2 value2;
};

int main() {
    UnionTemplate<int, float> u;
    u.value1 = 10;
    std::cout << "As int: " << u.value1 << std::endl;
    u.value2 = 3.14f;
    std::cout << "As float: " << u.value2 << std::endl;
    return 0;
}
13.4 特点
  1. 内存共享:所有成员共享同一块内存。
  2. 灵活性:可以存储不同类型的数据。
  3. 节省空间:只占用最大成员的内存空间。
13.5 使用注意事项
  1. 类型安全:使用时需要记住当前存储的是哪种类型,避免类型混淆。
  2. 构造和析构:共用体不会自动调用构造函数和析构函数。
  3. 复杂类型:对于包含非平凡构造函数或析构函数的类型,使用需要特别小心。
13.6 高级示例:类型安全的联合模板
#include <algorithm>  // 为 std::max 函数
#include <iostream>   // 用于输入输出
#include <string>     // 用于 std::string
#include <type_traits>  // 用于 std::is_same
#include <stdexcept>  // 用于 std::runtime_error

// SafeUnion 类模板,支持多种类型
template <typename... Types>
class SafeUnion {
private:
    // 辅助函数:计算所有类型中的最大大小
    template<typename... Ts>
    static constexpr std::size_t max_sizeof() {
        return std::max({sizeof(Ts)...});
    }

    // 存储区,大小为最大类型的大小,并按所有类型对齐
    alignas(Types...) unsigned char storage[max_sizeof<Types...>()];
    int typeIndex = -1;  // 当前存储的类型索引,-1 表示未初始化

    // 辅助函数:获取特定类型在模板参数包中的索引
    template<typename T, typename First, typename... Rest>
    static constexpr int getIndex() {
        if constexpr (std::is_same_v<T, First>)
            return 0;
        else
            return 1 + getIndex<T, Rest...>();
    }

public:
    SafeUnion() {}

    // 设置值的函数模板
    template<typename T>
    void set(const T& value) {
        typeIndex = getIndex<T, Types...>();  // 更新类型索引
        new (storage) T(value);  // 在存储区构造新对象
    }

    // 获取值的函数模板
    template<typename T>
    T& get() {
        if (getIndex<T, Types...>() != typeIndex) {
            throw std::runtime_error("Type mismatch");  // 类型不匹配时抛出异常
        }
        return *reinterpret_cast<T*>(storage);  // 返回正确类型的引用
    }

    ~SafeUnion() {}
};

int main() {
    // 创建一个可以存储 int, float, 或 string 的 SafeUnion
    SafeUnion<int, float, std::string> u;

    // 测试 int 类型
    u.set(10);
    std::cout << "As int: " << u.get<int>() << std::endl;

    // 测试 float 类型
    u.set(3.14f);
    std::cout << "As float: " << u.get<float>() << std::endl;

    // 测试 string 类型
    u.set(std::string("Hello"));
    std::cout << "As string: " << u.get<std::string>() << std::endl;

    // 测试类型不匹配的情况
    try {
        std::cout << u.get<int>() << std::endl;  // 将抛出异常,因为当前存储的是 string
    } catch (const std::exception& e) {
        std::cout << "Exception: " << e.what() << std::endl;
    }

    return 0;
}
13.7 应用场景
  1. 变量类型转换:在不同类型之间进行快速转换。
  2. 内存优化:当需要在不同时间使用不同类型,但不需要同时使用时。
  3. 网络编程:处理不同类型的网络数据包。
  4. 嵌入式系统:在资源受限的环境中优化内存使用。
13.8 优点
  1. 内存效率高。
  2. 可以灵活处理不同类型的数据。
  3. 结合模板使用时,提供了很大的灵活性。
13.9 缺点
  1. 类型安全性较低,容易出错。
  2. 对于复杂类型(如具有构造函数和析构函数的类),使用起来比较棘手。
  3. 可能导致代码可读性降低。
13.10 与 std::variant 的比较
  • std::variant(C++17引入)提供了一个类型安全的联合类型。
  • 相比共用体模板,std::variant 更加安全和易用,但可能会占用稍多的内存。
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐