25. 特例化

模板参数和实参

模板类型接收参数:

  • “类型类型”的类型参数
  • 内置类型值参数,如 int ​和函数指针
  • “模板类型”的模板参数

一个模板可以接受固定数量或可变数量的参数。

注意,一种很常见的模板类型实参命名方法是采用首字母大写的短名字,如 T​、C​、Cont​ 和 Ptr​。这是可以接受的,因为这种名字按惯例常用于相对较小的作用域。但是,当使用 ALL_CAPS​ 这种名字时,就很可能同宏名冲突。因此不要使用太长的名字,以免与宏名冲突。

类型作为实参

通过使用 typename ​或 class ​关键字,可以将一个模板实参定义为类型参数(type parameter)。语法上,typename ​和 class ​的作用相同,具体示例如下:

template<typename T>
void f(T);

类型实参是无约束的。换句话说,模板参数可以是任何类型,并没有限制它必须是某种特定类型或者类层次的一部分。在使用时,模板类和函数会根据传入的实参类型进行推断,生成特定的实现。

模板的类型参数可以是用户定义类型或者内置类型,C++ 不会因此产生额外的空间开销。

一个类型必须在作用域内且可访问,才能作为模板实参。

值作为实参

非类型或模板的模板参数称为值实参,传递给它的实参成为值实参。

template<typename T, int max>
class Buffer {
    T v[max];
public:
    Buffer() { }
    // ...
};

传递给模板的值参数可以是以下几种类型:

  • 整型常量表达式。
  • 指向外部链接的对象或函数的指针或引用。
  • 指向非重载成员指针。
  • nullptr ​指针。

C++ 不允许将浮点数和字符串字面量作为模板实参。例如:

X<int, "BMW323Ci"> x1;   // 错误:字符串字面值作为模板实参
char lx2[] = "BMW323Ci";
X<int, lx2> x2;          // 正确:lx2 为外部链接

在上面的代码中,直接使用字符串字面量 "BMW323Ci" ​作为模板实参是非法的,而使用具有外部链接的字符数组则是合法的。这种限制是为了简化编译器的实现,并避免在模板参数中引入浮点和字符串等复杂类型。

模板中的值参数在模板内部是一个常量,因此试图修改该值是非法的。例如:

template<typename T, int max>
class Buffer {
    T v[max];
public:
    Buffer(int i) { max = i; }  // 错误:max 为模板值参数
    // ...
};

在模板参数列表中,类类型模板参数可以用作后续的参数类型。这一特性与默认模板实参结合使用特别有用。例如:

template<typename T, T default_value = T{} >
class Vec {
    // ...
};

Vec<int, 42> c1; // default_value = 42
Vec<int> c2; // default_value = 0

操作作为实参

C++ 标准库中的 map ​容器需要为键值(Key)提供一个比较准则。通常来说,容器无法对其元素的键值定义固定的比较规则,因为不同类型的键值可能需要不同的比较方式。因此,map ​默认会使用 < ​运算符进行元素比较,但并非所有的 Key ​类型都支持 < ​运算符。

为了灵活地支持不同类型的键值,map ​允许用户自定义比较准则。可以通过以下两种方式实现:

  1. 使用一个模板值参数(例如,传递一个指向比较函数的指针)。
  2. 通过 map ​模板的一个类型实参,指定一个比较对象的类型。

使用一个模板值参数(比较函数指针)

一种方式是传入特定类型的比较对象。示例代码如下:

template<typename Key, typename V, bool (*cmp)(const Key&, const Key&)>
class map {
public:
    map();  // ...
};

如果我们定义了一个不区分大小写的字符串比较函数 insensitive​,可以如下使用:

bool insensitive(const string& x, const string& y) {
    // 忽略大小写比较
    return tolower(x) < tolower(y);
}

map<string, int, insensitive> m;  // 使用 insensitive 进行比较

在上面的示例中,map ​通过传入的 insensitive ​函数实现了不区分大小写的字符串比较。但这种方法的灵活性有限,因为 map ​的设计者必须确定使用一个函数指针来比较 Key​。

使用类型参数作为比较规则

更常用的做法是将比较准则作为模板类型参数传递,C++ 标准库中的 map ​也是采用这种方式。可以使用类似以下的代码:

template<typename Key, typename V, typename Compare = std::less<Key>>
class map {
public:
    map();                  // 使用默认比较
    map(Compare c);         // 用特定比较规则初始化
    Compare cmp {};         // 默认比较
    // ...
};

在上面代码中,std::less<Key> ​是默认的比较规则。如果用户希望使用不同的比较规则,可以通过自定义的 Compare ​类型替代默认的 std::less​。例如:

map<string, int> m1;                   // 使用默认 less<string> 比较
map<string, int, std::greater<string>> m2;  // 使用 greater<string> 比较

这种方式相比直接传入函数指针更加灵活,因为 Compare ​可以是任何类型的函数对象,而不仅仅是一个函数指针。

函数对象的使用

函数对象(functor)可以携带状态,这使得它比简单的函数指针更为灵活。例如:

struct Complex_compare {
    string locale;
    Complex_compare(string loc) : locale(loc) {}
    bool operator()(const string& a, const string& b) const {
        // 使用 locale 进行比较
    }
};
map<string, int, Complex_compare> m3({"French"});

在这里,Complex_compare ​是一个包含状态(locale​)的函数对象,允许比较函数根据 locale ​的不同实现不同的比较规则。

lambda 表达式的使用

我们还可以使用 lambda ​表达式来定义比较函数对象,例如:

using Cmp = bool(*)(const string&, const string&);
map<string, int, Cmp> m4(insensitive);
map<string, int, Cmp> m4([](const string& a, const string& b) { return a > b; });

auto cmp = [](const string& x, const string& y) { return x < y; };
map<string, int, decltype(cmp)> c4(cmp);

模板作为实参

在某些情况下,将一个模板(而不是类或值)作为模板实参传递给另一个模板是有用的。以下代码展示了如何将模板作为实参传递:

template<typename T, template<typename> class C>
class Xrefd {
    C<T> mems;   // 使用模板 C 生成 T 类型的对象
    C<T*> refs;  // 使用模板 C 生成 T* 类型的对象
    // ...
};

在上面的代码中,Xrefd ​模板的第二个模板参数 C ​是一个模板类,因此我们可以传递不同的模板(例如 vector​)来生成 Xrefd ​的不同实例。以下是具体的示例:

template<typename T>
class My_container {
    // 自定义容器的实现
};
Xrefd<Record, My_container> x2;  // 在 My_container 中存储 Record 的交叉引用

为了将一个模板用作模板实参,我们必须明确指出该模板的参数。例如,在定义 Xrefd ​时,我们指明 C ​是一个模板类,它接受单个类型参数。如果不这样做,我们就无法使用 C ​的特化版本。

只有类模板才能用作模板实参。

默认模板实参

类似于默认参数实参,我们只能对尾部模板参数指定和提供默认实参。

#include <string>
using std::string;

template<typename Target = string, typename Source = string>
Target to(const Source& arg)
{
    stringstream interpreter;
    Target result;

    if (!(interpreter << arg) || !(interpreter >> result) || !(interpreter >> std::ws).eof()) {
        throw runtime_error("to<>() failed");
    }

    return result;
}

auto x1 = to<string, double>(1.2);    // 显式指定 Target 为 string, Source 为 double
auto x2 = to<string>(1.2);            // 省略 Source,推断为 double
auto x3 = to<>(1.2);                  // Target 推断为 string, Source 推断为 double
auto x4 = to(1.2);                    // 使用默认的模板参数,省略 <>

特例化

默认情况下,模板具有单一定义,适用于所有可能的模板实参组合。然而,某些设计场景下,设计者可能希望根据模板实参的不同选择不同的实现。例如,针对指针类型的实参使用特定的实现,或限制模板实参为特定基类的派生类指针。这类需求可以通过提供多个可选的模板定义来实现,编译器根据用户提供的模板实参选择合适的定义。这种技术称为用户自定义特例化(user-defined specialization),允许用户为模板提供特定的实现。

特例化技术主要用于有效控制代码膨胀,并且在不影响代码泛化性的前提下提供更优的性能。(为特定的类型,提供特定的优化)

特例化版本可提供与通用模板不同的实现方式:

#include <iostream>

template<typename T, int N>
class Matrix;              // T 的 N 维矩阵

template<typename T>
class Matrix<T, 0> {        // N==0 的特例化版本
public:
    T val;
    // ...
};

template<typename T>
class Matrix<T, 1> {        // N==1 的特例化版本
public:
    T* elem;
    int sz;                // 元素个数
    // ...
};

template<typename T>
class Matrix<T, 2> {        // N==2 的特例化版本
public:
    T* elem;
    int dim1;              // 行数
    int dim2;              // 列数
    // ...
};


int main() {
    // 定义一个0维的Matrix
    Matrix<int, 0> m0;
    m0.val = 42;  // 直接给定值
    std::cout << "0维矩阵的值: " << m0.val << std::endl;

    // 定义一个1维的Matrix (向量)
    Matrix<int, 1> m1;
    m1.sz = 5;  // 假设有5个元素
    m1.elem = new int[m1.sz] {1, 2, 3, 4, 5};  // 动态分配并初始化
    std::cout << "1维矩阵的值: ";
    for (int i = 0; i < m1.sz; ++i) {
        std::cout << m1.elem[i] << " ";
    }
    std::cout << std::endl;
    delete[] m1.elem;  // 释放内存

    // 定义一个2维的Matrix (矩阵)
    Matrix<int, 2> m2;
    m2.dim1 = 2;  // 行数
    m2.dim2 = 3;  // 列数
    m2.elem = new int[m2.dim1 * m2.dim2] {1, 2, 3, 4, 5, 6};  // 动态分配并初始化
    std::cout << "2维矩阵的值:" << std::endl;
    for (int i = 0; i < m2.dim1; ++i) {
        for (int j = 0; j < m2.dim2; ++j) {
            std::cout << m2.elem[i * m2.dim2 + j] << " ";
        }
        std::cout << std::endl;
    }
    delete[] m2.elem;  // 释放内存

    return 0;
}

完整特例化

#include <iostream>
#include <vector>
#include <typeinfo>

template <typename T>
class Test {
public:
	T value;
	// 初始化函数
	Test(T value) : value(value) {
		// 打印类型
		std::cout << "Type: " << typeid(T).name() << std::endl;
	}
};

// 针对 void* 的特例化
template <>
class Test<void*> {
public:
	void* value;
	// 初始化函数
	Test(void* value) : value(value) {
		// 打印类型
		std::cout << "Type: void*" << std::endl;
	}
};

// 测试代码
int main() {
	Test<int*> test1(nullptr);
	Test<float*> test2(nullptr);
	Test<void*> test4(nullptr);
	return 0;
}

运行输出:

Type: int * __ptr64
Type: float * __ptr64
Type: void*

前缀 template<>​ 表示这是一个不必指明模板参数的特例化版本。特例化版本所使用的模板实参由模板名后面括号 <> ​中的内容指定。即,<void*>​ 指出这个定义将用于所有 T​ 为 void*​ 的 Vector​。

完整特例化(complete specialization):在使用此特例化版本时不用再指定或推断任何模板参数。

部分特例化

#include <iostream>
#include <vector>
#include <typeinfo>

template <typename T>
class Test {
public:
	T value;
	// 初始化函数
	Test(T value) : value(value) {
		// 打印类型
		std::cout << "Type: " << typeid(T).name() << std::endl;
	}
};

// 部分特例化
template<typename T>
class Test<T*> {
public:
	T* value;
	Test(T* value) : value(value) {
		// 打印类型
		std::cout << "Type: T*" << std::endl;
	}
};

// 测试代码
int main() {
	Test<int*> test1(nullptr);
	Test<float*> test2(nullptr);
	Test<void*> test4(nullptr);
	return 0;
}

运行输出:

Type: T*
Type: T*
Type: T*

部分特例化:对一个模板的部分参数或特性进行特化,而不要求完全指定模板参数。

如何理解,对于特例化版本的定义来说:

template<typename T>​ 相当于声明一下后面要使用的类型参数,方便后面定义特例化时使用。

主模板

主模板是最通用的模板定义,用于提供默认的接口。所有特例化版本都基于主模板定义,并且在模板解析时,编译器会优先考虑主模板,只有在特定的模板实参与特例化版本匹配时,才会选择特例化版本。

为了定义特例化版本,通常需要先声明(或直接定义)主模板,例如:

template<typename T>
class List;  // 声明主模板,不是定义

template<typename T>
class List<T*>;  // 针对指针类型的特例化

如果主模板在程序中从未实例化,则无须定义。基于此,我们可以定义只接受几种特定实参组合的模板。

函数模板特例化

在 C++ 中,函数模板特例化可以对模板函数进行定制化,使其在处理特定参数类型时表现出不同的行为。函数模板的特例化在使用场景上不如类模板特例化常见,但在处理某些特定类型时非常有效。

template<typename T>
bool less(T a, T b) {
    return a < b;
}

template<>
bool less<const char*>(const char* a, const char* b) {
    return strcmp(a, b) < 0;  // 针对 const char* 类型的特例化
}

函数模板特例化在实际应用中的场景有限,但一个常见的用例是在无实参的函数中进行选择。例如,定义一个 max_value ​模板函数,用于获取不同数据类型的最大值:

template<typename T>
T max_value();  // 无定义

template<>
constexpr int max_value<int>() { return INT_MAX; }

template<>
constexpr char max_value<char>() { return CHAR_MAX; }

在上面的代码中,max_value ​模板函数针对 int ​和 char ​类型进行了特例化,以返回对应类型的最大值。这些特例化版本是 constexpr ​的,确保可以在编译期计算出结果。

template<typename Iter>
Iter my_algo(Iter p)
{
    auto x = max_value<Value_type<Iter>>();  // 根据迭代器的值类型选择特例化版本
    // ...
}

本文介绍了C++模板参数的三种类型:类型参数、值参数和操作参数。

1. **类型参数**:使用`typename`或`class`关键字定义,可以是任何类型,无约束。类型参数在模板内部无额外空间开销,且必须是作用域内且可访问的。

2. **值参数**:非类型或模板的模板参数,传递给它的实参成为值参数。值参数可以是整型常量表达式、指向外部链接的对象或函数的指针或引用、指向非重载成员指针、`nullptr`指针。C++不允许浮点数和字符串字面量作为模板值参数。值参数在模板内部是常量,不可修改。

3. **操作参数**:用于指定比较准则等操作。C++标准库中的`map`容器允许用户自定义比较准则。可以通过模板值参数传递比较函数指针,或通过类型参数传递比较对象类型。使用类型参数更灵活,因为可以传递任意类型的函数对象,而不仅仅是函数指针。函数对象可以携带状态,比简单函数指针更灵活。

总结来说,C++模板参数提供了高度的灵活性和通用性,允许开发者根据需要定制类型、常量值和操作。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇