C++ 泛型编程 是C++中最强大的特性之一,它允许我们编写与类型无关的代码。本文将深入探讨C++模板的各个方面,包括其编译机制、特化、概念等高级特性。
1. 模板基础
模板是C++实现泛型编程的核心机制。它允许我们编写可以处理不同数据类型的代码,而不需要为每种类型重写代码。
1.1 函数模板
函数模板是最基本的模板形式。让我们看一个简单的例子:
cpp
template<typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
// 使用示例
int main() {
int i = max(10, 20); // T 被推导为 int
double d = max(3.14, 2.72); // T 被推导为 double
return 0;
}
1.2 类模板
类模板允许我们定义可以处理不同数据类型的类:
cpp
template<typename T>
class Stack {
private:
std::vector<T> elements;
public:
void push(const T& element) {
elements.push_back(element);
}
T pop() {
T element = elements.back();
elements.pop_back();
return element;
}
bool empty() const {
return elements.empty();
}
};
// 使用示例
int main() {
Stack<int> intStack; // 整数栈
Stack<string> strStack; // 字符串栈
return 0;
}
1.3 变量模板
C++14引入了变量模板,允许我们定义模板变量:
cpp
template<typename T>
constexpr T pi = T(3.1415926535897932385);
int main() {
float f = pi<float>;
double d = pi<double>;
return 0;
}
1.4 Lambda函数与模板
C++14引入了泛型Lambda(Generic Lambda),允许在Lambda函数中使用模板。这使得Lambda函数更加灵活和强大。
1.5.1 多参数模板Lambda
cpp
// 多个auto参数
auto add = [](auto a, auto b) {
return a + b;
};
// 使用示例
auto result1 = add(1, 2); // 整数加法
auto result2 = add(1.5, 2.5); // 浮点数加法
auto result3 = add("Hello", " World"); // 字符串连接
1.5.2 C++20中的显式模板Lambda
C++20允许在Lambda函数中使用显式模板语法,这提供了更好的类型控制和文档性:
cpp
// C++20之前的写法(使用auto)
auto oldStyle = [](auto x) { return x * 2; };
// C++20中的显式模板语法
auto newStyle = []<typename T>(T x) { return x * 2; };
// 多参数模板
auto multiParam = []<typename T, typename U>(T a, U b) {
return a + b;
};
// 使用示例
int main() {
// 使用显式模板Lambda
auto result1 = newStyle(42); // T 被推导为 int
auto result2 = newStyle(3.14); // T 被推导为 double
// 使用多参数模板Lambda
auto sum1 = multiParam(1, 2.5); // T=int, U=double
auto sum2 = multiParam("Hello", 42); // T=const char*, U=int
return 0;
}
2. 模板编译机制
2.1 编译时机
C++模板采用"两阶段编译"机制:
- 模板定义阶段:检查模板语法
- 模板实例化阶段:生成具体类型的代码
cpp
template<typename T>
void process(T value) {
value.non_existent_method(); // 在定义阶段不会报错
}
int main() {
process(42); // 在实例化阶段才会报错
return 0;
}
2.2 选择性实例化
编译器对模板方法的处理方式:
- 虚方法:总是生成代码
- 非虚方法:只为实际调用的方法生成代码
示例:
cpp
template<typename T>
class Container {
public:
virtual void process() { /* 总是生成代码 */ }
void sort() { /* 只在调用时生成代码 */ }
void filter() { /* 只在调用时生成代码 */ }
};
Container<int> c;
c.sort(); // 只生成sort方法的代码
2.3 显式实例化
我们可以显式地告诉编译器实例化特定的模板:
cpp
template<typename T>
class MyClass {
// ... 类定义
};
// 显式实例化声明
extern template class MyClass<int>;
// 显式实例化定义
template class MyClass<int>;
3. 类模板实参推导 (CTAD)(Class Template Argument Deduction)
2.1 基本用法
cpp
// 传统方式
std::pair<int, double> p1(1, 2.0);
// 使用CTAD
std::pair p2(1, 2.0); // 自动推导为pair<int, double>
2.2 智能指针注意事项
cpp
// 错误:类型推导被禁用
std::unique_ptr ptr(new int(42)); // 编译错误
// 正确:使用make函数
auto ptr = std::make_unique<int>(42);
auto sptr = std::make_shared<std::string>("Hello");
2.3 自定义推导指引
cpp
template<typename T>
class StringWrapper {
public:
StringWrapper(T str) : data(str) {}
private:
T data;
};
// 自定义推导指引
StringWrapper(const char*) -> StringWrapper<std::string>;
StringWrapper(const wchar_t*) -> StringWrapper<std::wstring>;
// 使用示例
StringWrapper s1("Hello"); // 推导为StringWrapper<std::string>
StringWrapper s2(L"World"); // 推导为StringWrapper<std::wstring>
4. 模板特化
模板特化允许我们为特定的类型或类型组合提供专门的实现。特化分为两种:全特化和偏特化。
4.1 全特化
全特化是指为模板参数指定具体的类型,完全替换原始模板的实现。
4.1.1 类模板全特化
cpp
// 原始模板
template<typename T>
class Container {
public:
void process(T value) {
std::cout << "处理通用类型: " << value << std::endl;
}
};
// 全特化:为bool类型提供专门实现
template<>
class Container<bool> {
public:
void process(bool value) {
std::cout << "处理布尔值: " << (value ? "true" : "false") << std::endl;
}
};
// 使用示例
int main() {
Container<int> c1;
c1.process(42); // 输出:处理通用类型: 42
Container<bool> c2;
c2.process(true); // 输出:处理布尔值: true
return 0;
}
4.1.2 函数模板全特化
cpp
// 原始模板
template<typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
// 全特化:为const char*类型提供专门实现
template<>
const char* max(const char* a, const char* b) {
return (strcmp(a, b) > 0) ? a : b;
}
// 使用示例
int main() {
int i = max(10, 20); // 使用原始模板
const char* s = max("hello", "world"); // 使用特化版本
return 0;
}
4.2 偏特化
偏特化是指为模板参数指定部分具体的类型,或者指定类型之间的关系。注意:函数模板不支持偏特化,只有类模板支持。
4.2.1 指针类型的偏特化
cpp
// 原始模板
template<typename T>
class Container {
public:
void process(T value) {
std::cout << "处理值: " << value << std::endl;
}
};
// 偏特化:为所有指针类型提供专门实现
template<typename T>
class Container<T*> {
public:
void process(T* value) {
std::cout << "处理指针: " << *value << std::endl;
}
};
// 使用示例
int main() {
Container<int> c1;
int x = 42;
c1.process(x); // 输出:处理值: 42
Container<int*> c2;
c2.process(&x); // 输出:处理指针: 42
return 0;
}
4.2.2 多参数模板的偏特化
cpp
// 原始模板
template<typename T, typename U>
class Pair {
public:
void print() {
std::cout << "通用Pair" << std::endl;
}
};
// 偏特化:当两个类型相同时
template<typename T>
class Pair<T, T> {
public:
void print() {
std::cout << "相同类型Pair" << std::endl;
}
};
// 偏特化:当第二个类型是第一个类型的指针时
template<typename T>
class Pair<T, T*> {
public:
void print() {
std::cout << "指针类型Pair" << std::endl;
}
};
// 使用示例
int main() {
Pair<int, double> p1; // 使用原始模板
p1.print(); // 输出:通用Pair
Pair<int, int> p2; // 使用第一个偏特化
p2.print(); // 输出:相同类型Pair
Pair<int, int*> p3; // 使用第二个偏特化
p3.print(); // 输出:指针类型Pair
return 0;
}
4.3 特化的注意事项
-
函数模板只支持全特化,不支持偏特化。如果需要类似偏特化的功能,可以使用函数重载或SFINAE。
-
特化的声明顺序很重要:
- 必须先声明原始模板
- 然后才能声明特化版本
- 特化版本必须与原始模板的接口完全匹配
- 特化的常见用途:
- 为特定类型提供更高效的实现
- 处理特殊类型(如指针、引用)
- 提供类型特定的行为
- 优化特定类型组合的性能
5. 可变参数模板
可变参数模板(Variadic Templates)是 C++11 引入的一个重要特性,它允许模板接受任意数量的模板参数。这个特性在实现通用函数和类时非常有用。
5.1 基本语法
可变参数模板使用 ...
语法来表示可变数量的参数:
cpp
// 函数模板
template<typename... Args>
void print(Args... args);
// 类模板
template<typename... Types>
class Tuple;
5.2 参数包展开
参数包可以通过多种方式展开:
- 递归展开:
cpp
// 基本情况
void print() {
std::cout << std::endl;
}
// 递归情况
template<typename T, typename... Args>
void print(T first, Args... args) {
std::cout << first << " ";
print(args...); // 递归调用,展开剩余参数
}
// 使用示例
int main() {
print(1, 2.5, "hello", 'a'); // 输出:1 2.5 hello a
return 0;
}
- 折叠表达式(C++17):
cpp
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // 右折叠
// 或者使用左折叠
// return (... + args);
}
// 使用示例
int main() {
auto result = sum(1, 2, 3, 4, 5); // 计算 1 + 2 + 3 + 4 + 5
return 0;
}
5.3 实际应用示例
- 完美转发:
cpp
template<typename... Args>
class Logger {
public:
template<typename... ConstructorArgs>
Logger(ConstructorArgs&&... args) {
// 完美转发所有参数
log("Constructor called with", std::forward<ConstructorArgs>(args)...);
}
private:
template<typename... LogArgs>
void log(LogArgs&&... args) {
(std::cout << ... << args) << std::endl;
}
};
- 元组实现:
cpp
template<typename... Types>
class Tuple;
// 基本情况:空元组
template<>
class Tuple<> {};
// 递归情况:非空元组
template<typename Head, typename... Tail>
class Tuple<Head, Tail...> : private Tuple<Tail...> {
public:
Tuple(const Head& head, const Tail&... tail)
: Tuple<Tail...>(tail...), head_(head) {}
Head& getHead() { return head_; }
Tuple<Tail...>& getTail() { return *this; }
private:
Head head_;
};
// 使用示例
int main() {
Tuple<int, double, std::string> t(1, 2.5, "hello");
std::cout << t.getHead() << std::endl;
return 0;
}
- 函数包装器:
cpp
template<typename Func, typename... Args>
auto callWithLogging(Func&& func, Args&&... args) {
std::cout << "Calling function with " << sizeof...(args) << " arguments" << std::endl;
return std::forward<Func>(func)(std::forward<Args>(args)...);
}
// 使用示例
int add(int a, int b) { return a + b; }
int main() {
auto result = callWithLogging(add, 1, 2);
return 0;
}
5.4 参数包操作
- 获取参数包大小:
cpp
template<typename... Args>
void printSize(Args... args) {
std::cout << "Number of arguments: " << sizeof...(args) << std::endl;
}
- 条件展开:
cpp
template<typename... Args>
void printIf(Args... args) {
(void)((std::cout << args << " ", 0) + ...); // 使用逗号运算符
std::cout << std::endl;
}
5.5 注意事项
-
递归展开可能导致编译时间增加,应谨慎使用。
-
参数包展开时要注意参数顺序:
cpp
template<typename... Args>
void process(Args... args) {
// 注意参数展开顺序
(processSingle(args), ...); // 从左到右
// 或者
(..., processSingle(args)); // 从右到左
}
- 在类模板中使用时,需要注意继承和成员变量的布局:
cpp
template<typename... Bases>
class Derived : public Bases... {
// 多重继承,每个基类都是参数包中的一个类型
};
- 在函数模板中使用时,要注意参数的生命周期:
cpp
template<typename... Args>
void store(Args&&... args) {
// 使用完美转发保存参数
(storage.emplace_back(std::forward<Args>(args)), ...);
}
5.6 高级应用
- 类型列表操作:
cpp
template<typename... Types>
struct TypeList {};
// 获取第一个类型
template<typename T, typename... Rest>
struct First {
using type = T;
};
// 获取最后一个类型
template<typename T>
struct Last {
using type = T;
};
template<typename T, typename... Rest>
struct Last<T, Rest...> {
using type = typename Last<Rest...>::type;
};
- 条件编译:
cpp
template<typename... Args>
struct AllIntegral {
static constexpr bool value = (std::is_integral_v<Args> && ...);
};
template<typename... Args>
void processIfIntegral(Args... args) {
if constexpr (AllIntegral<Args...>::value) {
// 处理整数参数
}
}
6. Type Traits
Type Traits 提供了在编译时检查和操作类型的能力:
- 一个模板化的结构,通常以的类型特征命名。例如 is_integer is_pointer is_void 等等
- 结构包含一个静态
const bool
命名值 - 对特征的结构进行特化,并且把它们的布尔值设置为一个合理的状态值
- 查询其值来使用类型特征,如:
my_type_trait :: value
cpp
template <typename T>
struct is_swapable {
static const bool value = false;
};
template <>
struct is_swapable<unsigned short> {
static const bool value = true;
};
template <>
struct is_swapable<short> {
static const bool value = true;
};
cpp
template<typename T>
void process(T value) {
if constexpr (std::is_integral_v<T>) {
// 处理整数类型
} else if constexpr (std::is_floating_point_v<T>) {
// 处理浮点类型
} else {
// 处理其他类型
}
}
一般类型特征(Primary Type Categories)
std::is_void std::is_integral std::is_floating_point std::is_array std::is_pointer std::is_lvalue_reference std::is_rvalue_reference std::is_enum std::is_union std::is_class std::is_function
复合类型特征(Composite Type Categories)
std::is_arithmetic std::is_fundamental std::is_object std::is_scalar std::is_compound std::is_reference std::is_member_pointer
C++17 引入了变量模板(variable template),为类型特征提供了更简洁的使用方式:
- 传统方式(C++11):
cpp
template<typename T>
void process(T value) {
if constexpr (std::is_integral<T>::value) {
// 处理整数类型
}
}
- 变量模板方式(C++17):
cpp
template<typename T>
void process(T value) {
if constexpr (std::is_integral_v<T>) {
// 处理整数类型
}
}
这两种方式是等价的,std::is_integral_v<T>
实际上就是 std::is_integral<T>::value
的简写。变量模板的引入使得代码更加简洁和易读。
7. Concepts (C++20)
Concepts 是 C++20 引入的一个重要特性,它提供了一种在编译时约束模板参数的方式,使得模板代码更加清晰、可读和易于维护。
7.1 Concepts 基础
Concepts 本质上是一个编译时谓词,用于检查类型是否满足特定的要求。它可以:
- 约束模板参数
- 提供更好的错误信息
- 简化模板代码
- 提高代码的可读性和可维护性
基本语法:
cpp
// 定义一个概念
template<typename T>
concept Number = std::is_arithmetic_v<T>;
template <typename T>
concept C = sizeof(T) == 4;
// 使用概念约束模板
template<Number T>
T add(T a, T b) {
return a + b;
}
// 使用示例
int main() {
add(1, 2); // 正确:int 满足 Number 概念
add(1.5, 2.5); // 正确:double 满足 Number 概念
add("a", "b"); // 错误:const char* 不满足 Number 概念
return 0;
}
7.2 组合概念
Concepts 可以通过逻辑运算符组合:
cpp
// 定义多个概念
template<typename T>
concept Integral = std::is_integral_v<T>;
template<typename T>
concept Signed = std::is_signed_v<T>;
// 组合概念
template<typename T>
concept SignedIntegral = Integral<T> && Signed<T>;
// 使用组合概念
template<SignedIntegral T>
T absolute(T value) {
return value < 0 ? -value : value;
}
7.3 requires
Concepts 可以使用要求表达式(requires-expression)来定义更复杂的约束:
- 简单要求(Simple Requirement): 验证表达式是否能通过编译
cpp
// 简单要求是一个表达式声明,要求该表达式是合法的。它主要用于检查某个操作或函数调用的有效性。
template<typename T>
concept HasIncrement = requires(T a) {
++a; // 简单要求:a 必须支持前置自增操作
};
- 类型要求(Type Requirement): 验证特定类型是否有效
cpp
// 类型要求使用 typename 关键字指定一个类型,要求该类型是合法的。它主要用于检查某个类型表达式是否代表一个有效的类型。
template<typename T>
concept HasIterator = requires {
typename T::iterator; // 类型要求:T 必须有一个名为 iterator 的嵌套类型
};
- 复合要求(Compound Requirement): 验证某些东西不会抛出异常或验证某个方法是否返回某个类型
cpp
// 复合要求用于检查表达式的类型、值分类(例如,lvalue 或 rvalue),以及(可选地)期望的返回类型。复合要求以花括号包围一个表达式,并可使用 noexcept 指定表达式必须是无异常的,也可以用 -> 指定表达式的返回类型。
#include <concepts>
template<typename T>
concept Swappable = requires(T a, T b) {
{ std::swap(a, b) } noexcept; // 复合要求:调用 std::swap 必须合法且无异常
{ a < b } -> convertible_to<bool>;
};
- 嵌套要求(Nested Requirement)
cpp
// 嵌套要求允许在 requires 表达式中使用另一个 requires 表达式。这种要求形式可以包含任何有效的 requires 表达式,它使得可以构建更加复杂和结构化的约束条件。
template<typename T>
concept Incrementable = requires(T a) {
requires sizeof(T) == 4;
++a; // 嵌套要求:a 必须支持前置自增操作
};
7.4 概念约束的多种形式
- 在模板参数列表中:
cpp
template<typename T>
requires Number<T>
T multiply(T a, T b) {
return a * b;
}
- 在函数声明中:
cpp
template<typename T>
T divide(T a, T b) requires Number<T> {
return a / b;
}
- 使用简写语法:
cpp
template<Number T>
T subtract(T a, T b) {
return a - b;
}
7.5 实际应用示例
cpp
// 定义一个可比较的概念
template<typename T>
concept Comparable = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>;
{ a > b } -> std::convertible_to<bool>;
{ a == b } -> std::convertible_to<bool>;
};
// 定义一个可排序的容器概念
template<typename T>
concept SortableContainer = requires(T container) {
requires Comparable<typename T::value_type>;
{ container.begin() } -> std::same_as<typename T::iterator>;
{ container.end() } -> std::same_as<typename T::iterator>;
{ std::sort(container.begin(), container.end()) };
};
// 使用这些概念实现排序函数
template<SortableContainer Container>
void sortContainer(Container& container) {
std::sort(container.begin(), container.end());
}
// 定义一个可迭代的概念
template<typename T>
concept Iterable = requires(T t) {
t.begin(); // 必须有 begin() 方法
t.end(); // 必须有 end() 方法
{ *t.begin() } -> std::same_as<typename T::value_type>; // 迭代器解引用类型必须匹配
};
// 定义一个可加的概念
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::same_as<T>; // 必须支持加法操作
{ a += b } -> std::same_as<T&>; // 必须支持 += 操作
};
// 使用这些概念
template<Iterable T>
void printContainer(const T& container) {
for (const auto& item : container) {
std::cout << item << " ";
}
}
template<Addable T>
T sum(T a, T b) {
return a + b;
}
// 使用示例
int main() {
std::vector<int> numbers = {3, 1, 4, 1, 5, 9};
sortContainer(numbers); // 正确:vector<int> 满足 SortableContainer 概念
std::list<std::string> words = {"hello", "world"};
sortContainer(words); // 正确:list<string> 满足 SortableContainer 概念
return 0;
}
8. SFINAE (Substitution Failure Is Not An Error)
SFINAE(Substitution Failure Is Not An Error)是C++模板编程中一个重要的特性,用于在模板实例化时进行条件编译。当一个模板的某个候选项在替换过程中失败(即不满足某些条件),这个失败并不会导致编译错误,而是简单地从候选项列表中移除。SFINAE常常与特化和重载结合使用,以达到根据不同条件选择不同实现的效果。接下来,我们看几个SFINAE的例子。
cpp
template<typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
process(T value) {
return value * 2;
}
template<typename T>
typename std::enable_if<std::is_floating_point<T>::value, T>::type
process(T value) {
return value * 1.5;
}
- 通过返回类型判断成员函数是否存在:
利用SFINAE来检测一个类型是否包含某个特定的成员函数
cpp
#include <type_traits>
#include <iostream>
// 声明一个类型检测的结构体,等价于false
template<typename, typename T>
struct has_serialize : std::false_type {};
// 萃取特化,利用SFINAE检测serialize方法
template<typename T>
struct has_serialize<T, decltype((void) std::declval<T>().serialize(), void())> : std::true_type {};
struct Serializer {
void serialize() const {}
};
struct NonSerializer {};
template<typename T>
void serializeIfPossible(const T& obj) {
if constexpr (has_serialize<T, void>::value) {
obj.serialize();
std::cout << "Serialized" << std::endl;
} else {
std::cout << "Serialization not available" << std::endl;
}
}
int main() {
Serializer s;
NonSerializer ns;
serializeIfPossible(s); // 输出:Serialized
serializeIfPossible(ns); // 输出:Serialization not available
}
- 通过std::enable_if启用或禁用模板
std::enable_if是SFINAE的一个典型应用,可以用来根据条件启用或禁用函数模板。
cpp
#include <type_traits>
#include <iostream>
// 对于整数类型的重载
template<typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
max(T x, T y) {
return (x > y) ? x : y;
}
// 对于浮点类型的重载
template<typename T>
typename std::enable_if<std::is_floating_point<T>::value, T>::type
max(T x, T y) {
std::cout << "Floating point version called." << std::endl;
return (x > y) ? x : y;
}
int main() {
std::cout << max(3, 7) << std::endl; // 调用整数重载
std::cout << max(3.14, 1.59) << std::endl; // 调用浮点数重载,并打印额外信息
}
- 使用尾置返回类型和decltype进行SFINAE
尾置返回类型允许我们在返回类型中使用函数参数,与decltype结合可以用于SFINAE,对不同类型执行不同操作。
cpp
#include <iostream>
#include <vector>
// 对于容器类型,返回其大小
template<typename T>
auto getSize(const T& c) -> decltype(c.size(), size_t()) {
std::cout << "Container version called." << std::endl;
return c.size();
}
// 对于整数类型,直接返回该值
template<typename T>
auto getSize(const T& i) -> decltype(T(1+1), size_t()) {
std::cout << "Integer version called." << std::endl;
return i;
}
int main() {
std::vector<int> v = {1,2,3};
std::cout << getSize(v) << std::endl; // 调用容器重载
std::cout << getSize(42) << std::endl; // 调用整数重载
}
9. 最佳实践
- 使用类型别名简化模板:
cpp
template<typename T>
using Vector = std::vector<T>;
template<typename T>
using UniquePtr = std::unique_ptr<T>;
- 使用auto简化模板代码:
cpp
template<typename Container>
auto getFirst(const Container& c) -> decltype(c.front()) {
return c.front();
}
- 使用static_assert进行编译时检查:
cpp
template<typename T>
class Stack {
static_assert(std::is_copy_constructible_v<T>,
"T must be copy constructible");
// ...
};
10. 常见陷阱和解决方案
-
模板代码膨胀:
- 使用显式实例化控制代码生成
- 将通用代码抽取到非模板基类
-
编译时间优化:
- 使用前向声明
- 将模板实现分离到.cpp文件
- 使用预编译头文件
-
调试技巧:
- 使用static_assert进行类型检查
- 使用typeid打印类型信息
- 使用编译器特定的调试选项
总结
C++模板是一个强大而复杂的特性,它为我们提供了:
- 类型安全的泛型编程
- 编译时多态
- 零开销抽象
- 强大的元编程能力
通过合理使用模板,我们可以编写出既灵活又高效的代码。然而,模板的复杂性也要求我们在使用时保持谨慎,确保代码的可维护性和可读性。