入门C++模板编程(二) 类模板

我是FFZero,一个10年开发经验的cpper。

今天继续来入门C+模板编程。
上一篇 入门C++模板编程(一) 函数模板 我们讲了函数模板,今天补齐在模板更为常见的一种应用–类模板
类模板细节比较多,很多内容需要自己真正代码实现过才能领会其中的用法,这里只列出我认为很有用和实际项目中使用较多的用法。

什么是类模板?

C++模板类,也称为类模板(Class Template),是C++中一种强大的泛型编程工具。它允许程序员定义一个通用的类蓝图,这个蓝图可以被实例化为处理不同数据类型的多个具体类。通过使用类模板,我们可以编写出更加灵活、可重用且高效的代码。
类模板本质上是一个参数化的类定义,它将类型作为参数,使得类能够操作多种不同的数据类型。当类模板被实例化时,编译器会根据提供的具体类型生成相应的类。这种机制类似于函数模板。

类模板的一般形式如下:

template <typename T>
class ClassName {
    // 类成员
    T m_t;
};

//实例化类模板
ClassName<int> c1;    
ClassName<double> c2;

这里的 T 是一个类型参数,代表任何有效的数据类型。关键字 typename 用来表明后面的标识符是一个类型名称;也可以使用 class 关键字,两者在大多数情况下是可以互换使用的。

类模板怎么用?

类模板语法

下面是一段模板类的典型用法,请先阅读,再结合下面的注意点自己实现一下:

//T的默认类型为int
template<class T = int> 
class AA {
public:
    T _a;
    
    AA(T a): _a(a) {}
    T add() { 
        T x = 1; 
        return x + _a;
    }
    
    T get();
};

template<class T>  //类模板的成员函数在类外实现
T AA<T>::get() {          
    return _a;
}

int main() {
	AA<int> a(1);  //指定类型int
	AA<>  b(2);   //使用默认类型int实例化
	
	auto c = new AA<double>(3);    //auto推导出c的类型:class AA<dobule>
	delete c;
}

下面是使用模板类的几个注意点:

  1. 类模板可以为通用参数指定缺省的数据类型(C++11以后函数模板也可以)
  2. 创建对象时,必须指明具体的数据类型,就算使用默认类型也需要<>
  3. 类模板的成员函数可以在类外实现,需要在定义前加上 template<>和相应的模板参数列表

嵌套使用类模板

模板特化是一个非常重要和实用的使用方法。

嵌套使用类模板意味着一个类模板包含另一个类模板作为其成员。在实现诸如容器、映射、树结构等数据结构时特别有用。

典型例子:

// 创建一个 3x4 的二维整数向量
std::vector<std::vector<int>> matrix(3, std::vector<int>(4));
  • 外层 vector: 这是外部的 vector 类模板实例化,它存储的是 vector< int > 类型的对象。
  • 内层 vector< int > : 每个元素都是一个 vector 类模板实例化,其中每个元素都是 int 类型的数据。

模板类的特化

C++模板类的特化是一种机制,它允许程序员为特定类型提供一个特别版本的模板实现。这种特化可以是全特化(完全指定所有模板参数)或偏特化(部分指定模板参数)。通过特化,我们可以针对某些特殊的数据类型提供更高效的实现或者处理一些特殊情况,而不需要改变通用模板的行为。

全特化 (Full Specialization)

全特化意味着所有的模板参数都被明确地指定了。当使用全特化时,编译器会根据提供的具体类型来生成一个新的独立的类定义。这个新类与原模板类在逻辑上是独立的,除了名字相同外,它们之间没有其他关联。

假设有一个通用的存储类 Storage,它可以存储任意类型的8个元素:

template <typename T>
class Storage {
    T data[8];
public:
    void set(size_t index, const T& value) { data[index] = value; }
    T get(size_t index) const { return data[index]; }
};

// 针对 bool 类型的全特化
template <>
class Storage<bool> {
    unsigned char data;  // 使用位来存储布尔值
public:
    void set(size_t index, bool value) { data = (data & ~(1 << index)) | (value << index); }
    bool get(size_t index) const { return (data >> index) & 1; }
};

int main() {
    Storage<bool> c1; //使用全特化版本
    Storage<int> c2; //使用普通版本
}

在这个例子中,我们为 bool 类型提供了 Storage 的一个全特化版本,利用了位运算来高效地存储布尔值,而不是用单独的字节来存储每个布尔值

偏特化 (Partial Specialization)

偏特化允许只对部分模板参数进行指定,而其他的参数保持通用。这通常用于处理一些特定的情况,例如当多个模板参数中有某些特定的关系时。

考虑一个容器类,它有两个类型参数,并且我们想要对其中一个参数是另一个参数的引用的情况进行优化:

template <typename T1, typename T2>
class Container {
    T1 item1;
    T2 item2;
public:
    Container(T1 t1, T2 t2) : item1(t1), item2(t2) {}
    void print() const;  // 声明
};

template <typename T1, typename T2>
void Container<T1, T2>::print() const {
    std::cout << "Generic: " << item1 << " : "  << item2 << std::endl;
}

// 当 T2 是 T1 的引用时的偏特化
template <typename T>
class Container<T, T&> {
    T item1;
    T& item2;
public:
    Container(T t1, T& t2) : item1(t1), item2(t2) {}
    // 可以添加额外的方法或成员变量
    void print() const;  // 声明
};

template <typename T>
void Container<T, T&>::print() const {
    std::cout << "Specialized: " << item1 << " : " << item2 << std::endl;
}
int main() {
     int i = 2;
	 Container<int, double> c1(1, i);    // 普通版本
	 c1.print();                        //Generic: 1 : 2
	 Container<int, int&> c2(1, i);     // 特化版本
	 c2.print();                        //Specialized: 1 : 2
}

这里,Container<T, T&> 是一个偏特化版本,它仅在第二个参数是第一个参数的引用时才被使用。我们在类外分别定义了通用模板和偏特化版本的 print 函数。

模板版本优先级

  1. 优先级:全特化的类优先于偏特化的类,偏特化的类优先于没有特化的类

进阶用法:函数类型模板特化

  1. 典型用法

请看下面的代码,体会函数类型模板特化的神奇之处:
在这里插入图片描述
这是ZLMediakKit中实现的一个模板结构体 function_traits,用于提取和分析各种函数类型的特征,是C++模板元编程的一个经典示例。它展示了如何对不同种类的函数类型(普通函数、函数指针、std::function、成员函数、函数对象)进行统一处理,并提取函数的返回类型、参数数量、参数类型等信息。
想学习ZLMediakKit的也可以关注我的专栏,一起通过该项目巩固C++。

  1. 用法解析

下面抽取它的核心部分进行解析:

//通用模板类function_traits,没有定义具体的实现
template<class T> 
 class function_traits; 
    
// 对模板类function_traits进行了特化,使其针对函数类型进行特殊处理
// RetType 表示函数的返回类型
// Args... 是一个参数包,用于表示函数的参数类型列表
// A<RetType(Args...)> 是对模板类function_traits的特化版本,只适用于函数类型的模板实例化
template<class RetType, class... Args> 
class function_traits<RetType(Args...)> {
public:
    enum { arity = sizeof...(Args) };
    typedef Ret function_type(Args...);
    typedef Ret return_type;
    using stl_function_type = std::function<function_type>;
    typedef Ret(*pointer)(Args...);
};
  1. 使用方法
int foo(double, char) {
    return 42;
}
 
int main() {
    using traits = function_traits<int(double, char)>;
    // 获取函数的返回类型
    using return_type = traits::return_type;
    static_assert(std::is_same<return_type, int>::value, "Return type should be int");

    // 获取函数参数的个数
    constexpr size_t arity = traits::arity;
    std::cout << "The number of arguments: " << arity << std::endl;
}

在这个示例中,我们:

  • 使用 function_traits<int(double, char)> 提取了函数类型 int(double, char) 的特征。
  • 验证了返回类型为 int,参数个数为 2,第一个参数为 double,第二个参数为 char。
  • 通过 static_assert 在编译期验证类型匹配,通过 std::cout 输出相关信息。

类模板的继承

类模板也是可以继承的,下面是几种常见的使用场景。

  1. 模板类继承普通类
class A {
public:
     int _a;
     A(int a) : _a(a) { cout << "A constructor() \n"; }
     void func1() const{ cout << "func1(), _a=" << _a << endl; }
};
        
template<class T1, class T2>
class B : public A {
public:
     B(const T1 x, const T2 y, int a) :A(a), _x(x), _y(y) { cout << "B constructor() \n"; }
     void func2() { cout << "func2(), _x=" << _x << ", _y=" << _y << endl; }
    
     T1 _x; 
     T2 _y;
};

B<int, string> b(9, "good", 2);
b.func2();

和普通继承类似,子模板类B要实现A的构造函数

  1. 普通类继承模板类的实例版本
class C : public B<int, string> {
public:
     C(const int x, const string y, int c) : B<int, string>(x, y, c) {
         cout << "C constructor() \n";
     }
     void func3() { cout << "func3(), _x=" << _x << ", _y=" << _y << endl; }
};

函数类C继承了模板类B的实例版本,C要实现B的实例类构造函数

  1. 模板类继承模板类
template<class T1, class T2>
class A {
public:
     A(T1 x, T2 y) :_x(x), _y(y) { } 
     T1 _x;
     T2 _y;   
};
    
template<class T, class T1, class T2>
class B : public A<T1, T2> {
public:
    B(const T a, T1 x, T2 y) : A<T1, T2>(x, y), _a(a) { }
    
    //不加this->编译错误,编译器需要显式的提示来正确解析继承自基类模板的成员
    void func() {
         cout << "x=" << this->_x << ", y=" << this->_y << ", a=" << this->_a << endl;
    }
    T _a;
};

在这段代码中,编译器要求使用 this-> 来访问基类模板中的成员变量 _x 和 _y,否则会导致编译错误。这是因为模板类的依赖名称(dependent name)解析的规则,下面详细解释这种情况及其原因。

在模板类 B 的定义中,B 继承自模板基类 A<T1, T2>。因为 A 是一个模板类,T1 和 T2 是模板参数,因此基类的成员 _x 和 _y 被称为依赖名称(dependent names)。这些名称的解析依赖于模板参数的具体类型。

在C++模板编译的过程中,编译器需要区分依赖名称和非依赖名称:

  • 非依赖名称:在编译期就可以确定的名称,不依赖于模板参数。
  • 依赖名称:只有在模板实例化时,才能确定其含义或解析。

当编译器在处理模板类时(如 B),编译器在解析成员函数 func 时还不知道基类 A<T1, T2> 的具体类型,因为 T1 和 T2 是模板参数。因此,基类中的成员 _x 和 _y 对于编译器来说是依赖于模板参数的名称,不能在编译期直接解析。
在这种情况下,如果直接写 _x 和 _y,编译器会认为这是一个独立的非依赖名称,而不是基类的成员变量。因此需要显式地使用 this-> 来告诉编译器这些成员属于基类,并且是依赖名称。

  1. 模板类继承模板类型里的类型
template<class T> 
class EE: public T {
    EE(): T() { cout << "EE()" << endl; }
    EE(int a): T(a) { cout << "EE(int)" << endl; }
};

这种用法可以用来扩展或装饰现有的类行为,而不需要修改原类的定义。例如,类 EE 可以对模板参数 T 进行包装和增强,增加额外的功能。在这个例子中,EE 扩展了 T 的构造行为,在创建 EE 对象时输出了一些调试信息。

模板类的友元

在 C++ 中,友元函数是一种特殊的函数,它虽然不是某个类的成员函数,但却能够访问该类的所有私有和受保护成员。友元函数对于那些需要访问类的内部数据但又不适合作为类成员的函数来说非常有用。
友元最常见的用法是实现操作符重载。

下面的例子展示了如何使用友元函数来重载<<操作符,以便能够将自定义类的对象输出到标准输出流。

#include <iostream>

class Point {
private:
    int x, y;

public:
    Point(int x, int y) : x(x), y(y) {}
    // 声明友元函数
    friend std::ostream& operator<<(std::ostream& os, const Point& pt);
};

// 定义友元函数
std::ostream& operator<<(std::ostream& os, const Point& pt) {
    os << "(" << pt.x << ", " << pt.y << ")";
    return os;
}

int main() {
    Point p(1, 2);
    std::cout << p << std::endl; // 使用重载的 << 操作符
    return 0;
}

在非模板类中可以这么定义友元函数,但是普通友元一次只能指定一个指定模板参数的模板类,而不能将整个模板函数模板的所有实例都设为友元

#include <iostream>
using namespace std;

// 函数模板
template <typename T>
void foo(T value) {
    cout << "Value: " << value << endl;
}

class MyClass {
private:
    int data;

public:
    MyClass(int d) : data(d) {}

    // 让具体的 foo<int> 函数实例成为友元
    friend void foo<int>(int);
};

int main() {
    MyClass obj(42);
    foo(10); // 只在 foo<int> 可以访问 MyClass 的私有成员时有效
    // foo(3.14); // 错误:foo<double> 不是 MyClass 的友元
    return 0;
}
return 0;
}

如果希望将模板函数的所有可能的实例都设为友元,则可以使用友元函数模板。如下所示:


//声明友元函数模板
template<typename T>
void show(T& a); 
    
template<class T1, class T2> 
class AA {
public:
   AA(T1 x, T2 y):_x(x), _y(y) {}        
   //再次声明模板友元
   friend void show<>(AA<T1, T2>& a);
private:
   T1 _x;
   T2 _y;
};
    
template<typename T>
void show(T& a) {
    //通用版本,不仅模板类A可以用,模板类B也可以使用该版本;
    cout << "show(T& a)" << endl;
}
    
template<>
void show(AA<int, string>& a) {
    //特化版本;
     cout << "show(AA<int, string>& a)" << endl;
}
    
int main() {
    AA<char, string> a('a', "good");
    show(a);        //普通版本
    
    AA<int, string> b(1, "bad");
    show(b);       //AA<int, string>特化版本
}
  • 友元函数模板的声明:template void show(T& a); 声明了一个模板函数 show,它可以接受任何类型的引用作为参数
  • 模板类 AA 的定义:AA 是一个模板类,它有两个类型参数 T1 和 T2。在类内部,你声明了一个友元函数 show,并指定了它是一个模板函数。注意这里的语法 friend void show<>(AA<T1, T2>& a);,其中 <> 表示这是一个模板函数。
  • 通用版本的 show 函数:这是一个通用的模板函数,可以处理任何类型的引用。在这个例子中,它会输出 “show(T& a)”。
  • 特化版本的 show 函数:这是一个针对 AA<int, string> 类型的全特化版本。当调用 show 函数时,如果传入的是 AA<int, string> 类型的对象,将使用这个特化版本,输出 “show(AA<int, string>& a)”。

以上

以上就是类模板的一些入门知识了,可以看到用法很多很杂,细节也很多,但是配合好的IDE还是很容易掌握正确用法的。
关注我,一起解锁C++更多知识点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值