C++泛型编程(一):模板详解
1.泛型编程的优点
- 代码复用 :避免为不同类型写重复代码
- 类型安全 :编译期类型检查
- 性能优化 :模板在编译期展开,没有运行时开销
- 灵活性 :可以适应不同的数据类型
2.模板基础
1.函数模板
// 基本函数模板示例
template <typename T>
T max(T a, T b) {return a > b ? a : b;
}
- 编译器会为每个使用的类型生成对应的函数
- 可以显式指定类型:
max<int>(10, 20)
- 支持类型推导: max(10, 20),省略==
<int>
==
2.类模板
template <typename T>
class MVector {T* data;size_t size;
public:MVector() : data(nullptr), size(0) {}void push_back(const T& value);
};
- 必须显式指定类型:
MVector<int> vec;
- 成员函数在使用时才实例化
- 可以有多个类型参数
3.高级特性
1.模板特化
模板特化允许开发者为特定类型或类型组合提供专门的实现。当通用模板无法满足特定需求时,特化模板可以调整行为以处理特定的情况
C++ 支持全特化(Full Specialization) 和 偏特化(Partial Specialization),但需要注意的是,函数模板不支持偏特化,只能进行全特化
1.全特化
全特化是针对模板参数的完全特定类型组合。它提供了模板的一个特定版本,当模板参数完全匹配特化类型时,编译器将优先使用该特化版本
// 主模板
template <typename T>
class TypeInfo {
public:static const char* name() { return "unknown"; }
};// 完全特化
template <>
class TypeInfo<int> {
public:static const char* name() { return "int"; }
};template <>
class TypeInfo<std::string> {
public:static const char* name() { return "string"; }
};
2.偏特化
偏特化允许模板对部分参数进行特定类型的处理,同时保持其他参数的通用性
// 主模板
template <typename T, typename U>
class Pair {T first;U second;
public:Pair(const T& t, const U& u) : first(t), second(u) {}void print() { std::cout << first << ", " << second << std::endl; }
};// 指针类型的偏特化
template <typename T, typename U>
class Pair<T*, U*> {T* first;U* second;
public:Pair(T* t, U* u) : first(t), second(u) {}void print() { std::cout << *first << ", " << *second << std::endl; }
};
2.可变参数模板
可变参数模板允许模板定义中包含任意数量的模板参数。主要通过以下方式实现:
- 参数包(Parameter Pack):用 … 表示的一组参数
- 包展开(Pack Expansion):将参数包展开成单独的参数
1.函数模板实例
// 递归终止函数 - 处理参数包为空的情况
void print1() {std::cout << std::endl;
}// 可变参数模板函数
template<typename T, typename... Args> // Args是一个模板参数包
void print1(T first, Args... args) { // args是一个函数参数包std::cout << first << " "; // 处理第一个参数print(args...); // 递归处理剩余参数
}// 使用折叠表达式
template <typename... Args>
void print2(const Args&... args) {// 使用左折叠展开参数包,并在每个参数之后输出一个空格((std::cout << args << " "), ...);std::cout << std::endl;
}// 使用示例
print(1, "hello", 3.14, 'c'); // 可以接受任意数量和类型的参数
2.参数包的使用方法
template<typename... Args>
void example(Args... args) {// 获取参数包中的参数数量constexpr size_t size = sizeof...(Args);// 展开参数包的几种方式// 1. 直接展开func(args...); // 展开成 func(arg1, arg2, arg3)// 2. 使用初始化列表展开int arr[] = {(std::cout << args << " ", 0)...};// 3. 折叠表达式(C++17)(std::cout << ... << args);
}
3.类模板中的可变参数
// 基本的Tuple声明
template<typename... Types>
class Tuple;// 递归特化版本
template<typename First, typename... Rest>
class Tuple<First, Rest...> : private Tuple<Rest...> {First first; // 存储当前层级的值
public:// 构造函数接收当前值和剩余参数Tuple(First f, Rest... rest) : Tuple<Rest...>(rest...), // 递归构造基类first(f) {} // 初始化当前值
};// 特化的终止条件
template<>
class Tuple<> {}; // 空tuple作为递归终点// 使用示例
Tuple<int, string, double> t(1, "hello", 3.14);
3.模板折叠
折叠表达式的引入显著简化了处理参数包的过程。它们允许开发者直接对参数包应用操作符,而无需手动展开或递归处理参数。这不仅使代码更加简洁,还提高了可读性和可维护性
1.一元折叠
// 一元左折叠
template<typename... Args>
void print_left_init(Args... args) {(... + args); // ((arg1 + arg2) + arg3) + arg4
}// 一元右折叠
template<typename... Args>
void print_right_init(Args... args) {(args + ...); // (arg1 + (arg2 + (arg3 + arg4)))
}
2.二元折叠
// 二元左折叠
template<typename... Args>
void print_left_init(Args... args) {(0 + ... + args); // ((0 + arg1) + arg2) + arg3
}// 二元右折叠
template<typename... Args>
void print_right_init(Args... args) {(args + ... + 0); // (arg1 + (arg2 + (arg3 + 0)))
}
3.其他实例
template<typename T, typename... Args>
void pushToVector(std::vector<T>& v, Args... args) {(v.push_back(args), ...); // 逗号表达式折叠
}// 使用示例
std::vector<int> vec;
pushToVector(vec, 1, 2, 3, 4); // vec: [1,2,3,4]
4.空参数包处理
template<typename... Args>
auto sum(Args... args) {return (args + ...); // 空参数包会导致编译错误return (args + ... + 0); // 安全:空参数包返回0
}
4.常用技巧
1.SFINAE(Substitution Failure Is Not An Error)
SFINAE (Substitution Failure Is Not An Error) – 替换失败不是错误是C++模板编程中的一个重要概念,它允许在模板实例化失败时继续查找其他可能的重载
1.简单示例
// 检查类型是否有size()成员函数
template<typename T>
struct has_size {
private:// 测试size()是否存在template<typename U>static auto test(int) -> decltype(std::declval<U>().size(), std::true_type{});// 后备方案template<typename>static std::false_type test(...);public:static constexpr bool value = decltype(test<T>(0))::value;
};
2.enable_if
// 只接受整数类型的函数
template<typename T>
typename std::enable_if<std::is_integral<T>::value, bool>::type
is_odd(T i) {return bool(i % 2);
}// 只接受浮点类型的函数
template<typename T>
typename std::enable_if<std::is_floating_point<T>::value, bool>::type
is_odd(T x) {return false; // 浮点数没有奇偶性
}
3.if_constexpr(C++17)
template<typename T>
void process(const T& value) {if constexpr (std::is_integral_v<T>) {// 整数类型处理std::cout << "整数: " << value << std::endl;} else if constexpr (std::is_floating_point_v<T>) {// 浮点类型处理std::cout << "浮点数: " << value << std::endl;} else {// 其他类型std::cout << "其他类型" << std::endl;}
}
4.concepts(C++20)
// 定义一个 concept:要求类型必须是可输出到 std::ostream
template<typename T>
concept Printable = requires(T x) {{ std::cout << x } -> std::same_as<std::ostream&>;
};template<Printable T>
void print(const T& value) {std::cout << value << std::endl;
}
5.总结
SFINAE作为C++模板编程中的一项强大功能,通过在模板实例化过程中允许替换失败而不报错,实现了基于类型特性的编程。然而,SFINAE的语法复杂且难以维护,现代C++引入的新特性如概念等在某些情况下已经能够更简洁地实现类似的功能。尽管如此,理解SFINAE的工作机制依然对于掌握高级模板技术和阅读老旧代码具有重要意义
2.模板元编程
模板元编程是在编译期进行的计算和类型操作,主要用于:
- 编译期计算
- 类型转换和判断
- 代码生成
1.计算示例
// 1.编译期计算阶乘
template<unsigned N>
struct Factorial {static constexpr unsigned value = N * Factorial<N-1>::value;
};
// 递归终止
template<>
struct Factorial<0> {static constexpr unsigned value = 1;
};
// 使用
constexpr auto result = Factorial<5>::value; // 5! = 120// 2.斐波那契数列
template<unsigned N>
struct Fibonacci {static constexpr unsigned value = Fibonacci<N-1>::value + Fibonacci<N-2>::value;
};
template<>
struct Fibonacci<0> {static constexpr unsigned value = 0;
};
template<>
struct Fibonacci<1> {static constexpr unsigned value = 1;
};
2.类型操作
// 1.判断类型是否为指针
template<typename T>
struct IsPointer {static constexpr bool value = false;
};template<typename T>
struct IsPointer<T*> {static constexpr bool value = true;
};// 2.移除引用
template<typename T>
struct RemoveReference {using type = T;
};template<typename T>
struct RemoveReference<T&> {using type = T;
};template<typename T>
struct RemoveReference<T&&> {using type = T;
};
3.注意事项
- 编译时间:复杂的模板元编程会增加编译时间、应适度使用,避免过度复杂化
- 可读性:模板元编程代码往往难以理解、需要良好的文档和注释
- 调试难度:编译期错误信息复杂、调试工具支持有限
- 维护成本:代码复杂度高、修改需要谨慎
5.其他
本号文章仅为个人收集总结,强烈欢迎大佬与同好指误或讨论 ^^