C++模板
C++类模板
概述
C++ 模板(Templates)是现代 C++ 中强大而灵活的特性,支持泛型编程,使得代码更具复用性和类型安全性。
模板不仅包括基本的函数模板和类模板,还涵盖了模板特化(全特化与偏特化)、模板参数种类、变参模板(Variadic Templates)、模板元编程(Template Metaprogramming)、SFINAE(Substitution Failure Is Not An Error)等高级内容。
函数模板
基本语法
template <typename T>
T functionName(T param) {
// 函数体
}
代码实例
实例化的时候模板自动判断类型
#include <iostream>
template <typename T>
T maxValue(T a, T b) {
return (a > b) ? a : b;
}
int main() {
std::cout << maxValue(3, 7) << std::endl; // int 类型
std::cout << maxValue(3.14, 2.72) << std::endl; // double 类型
std::cout << maxValue('a', 'z') << std::endl; // char 类型
return 0;
}
类模板
类模板允许定义通用的类,通过类型参数化,实现不同类型的对象。
语法:
template <typename T>
class ClassName {
public:
T memberVariable;
// 构造函数、成员函数等
};
代码示例:
#include <iostream>
#include <string>
template <typename T, typename U>//模板前置声明
class Pair {
public:
T first;
U second;
Pair(T a, U b) : first(a), second(b) {}
void print() const {
std::cout << "Pair: " << first << ", " << second << std::endl;
}
};
int main() {
Pair<int, double> p1(1, 2.5);。//注意这里要声明两个类型都是什么
p1.print(); // 输出:Pair: 1, 2.5
Pair<std::string, std::string> p2("Hello", "World");//注意这里要声明两个类型都是什么
p2.print(); // 输出:Pair: Hello, World
Pair<std::string, int> p3("Age", 30);//注意这里要声明两个类型都是什么
p3.print(); // 输出:Pair: Age, 30
return 0;
}
模板参数
模板参数决定了模板的泛用性与灵活性。C++ 模板参数种类主要包括类型参数、非类型参数和模板模板参数。
类型参数
类型参数用于表示任意类型,在模板实例化时被具体的类型替代。
语法:
template <typename T>
class MyClass {
public:
T data;
};
非类型参数
非类型参数允许模板接受非类型的值,如整数、指针或引用。
相比于两个类型参数:非类型参数的优点是语义清晰,用法简单,编译阶段直接生成对应类型大小,不需要间接方式,代码效率高
template <typename T, int N>
class FixedArray {
public:
T data[N];
};
代码示例:固定大小的数组类
#include <iostream>
template <typename T, std::size_t N>
class FixedArray {
public:
T data[N];
T& operator[](std::size_t index) {//这里注意要用引用
//左值不可被修改,右值可被修改,加引用变为右值
return data[index];
}
void print() const {
for(std::size_t i = 0; i < N; ++i)
std::cout << data[i] << " ";
std::cout << std::endl;
}
};
int main() {
FixedArray<int, 5> arr;
for(int i = 0; i < 5; ++i)
arr[i] = i * 10;
arr.print(); // 输出:0 10 20 30 40
return 0;
}
模板模板参数(Template Template Parameters)
模板模板参数允许模板接受另一个模板作为参数。这对于抽象容器和策略模式等场景非常有用。
语法:
template <template <typename, typename> class Container>
class MyClass { /* ... */ };
实例代码,容器适配器
不用实例化Container里面的那两个类型,已经默认实例化好了 ,用的时候再去实例化,例如第17行,这时候container里面第一个参数是list第二个参数是allocator
#include <iostream>
#include <vector>
#include <list>
template <template <typename, typename> class Container, typename T>
class ContainerPrinter {
public:
void print(const Container<T, std::allocator<T>>& container) {//源码默认
for(const auto& elem : container)
std::cout << elem << " ";
std::cout << std::endl;
}
};
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::list<int> lst = {10, 20, 30};
ContainerPrinter<std::vector, int> vecPrinter;
vecPrinter.print(vec); // 输出:1 2 3 4 5
ContainerPrinter<std::list, int> listPrinter;
listPrinter.print(lst); // 输出:10 20 30
return 0;
}
模板特化
模板特化允许开发者为特定类型或类型组合提供专门的实现。当通用模板无法满足特定需求时,特化模板可以调整行为以处理特定的情况。C++ 支持**全特化(Full Specialization)**和 偏特化(Partial Specialization),但需要注意的是,函数模板不支持偏特化,只能进行全特化。
全特化
全特化是针对模板参数的完全特定类型组合。它提供了模板的一个特定版本,当模板参数完全匹配特化类型时,编译器将优先使用该特化版本。
// 通用模板
template <typename T>
class MyClass {
// 通用实现
};
// 全特化
template <>
class MyClass<SpecificType> {
// 针对 SpecificType 的实现
};
偏特化(Partial Specialization)
偏特化允许模板对部分参数进行特定类型的处理,同时保持其他参数的通用性。对于类模板而言,可以针对模板参数的某些特性进行偏特化;对于函数模板,则仅支持全特化,不支持偏特化。
// 通用模板
template <typename T, typename U>
class MyClass {
// 通用实现
};
// 偏特化:当 U 是指针类型时
template <typename T, typename U>
class MyClass<T, U*> {
// 针对 U* 的实现
};
函数模板的特化
与类模板不同,函数模板不支持偏特化,只能进行全特化。当对函数模板进行全特化时,需要显式指定类型。
#include <iostream>
#include <string>
// 通用函数模板
template <typename T>
void printValue(const T& value) {
std::cout << "General print: " << value << std::endl;
}
// 函数模板全特化
template <>
void printValue<std::string>(const std::string& value) {
std::cout << "Specialized print for std::string: " << value << std::endl;
}
int main() {
printValue(42); // 调用通用模板,输出:General print: 42
printValue(3.14); // 调用通用模板,输出:General print: 3.14
printValue(std::string("Hello")); // 调用全特化模板,输出:Specialized print for std::string: Hello
return 0;
}
注意事项
优先级:全特化版本的优先级高于通用模板,因此当特化条件满足时,总是选择特化版本。
显式指定类型:函数模板特化需要在调用时显式指定类型,或者确保类型推导可以正确匹配特化版本。
不支持偏特化:无法通过偏特化对函数模板进行部分特化,需要通过其他方法(如重载)实现类似功能。
变参模板
变参模板允许模板接受可变数量的参数,提供极高的灵活性,是实现诸如 std::tuple、std::variant 等模板库组件的基础。
语法
变参模板使用 参数包(Parameter Pack),通过 ... 语法来表示。
template <typename... Args>
class MyClass { /* ... */ };
template <typename T, typename... Args>
void myFunction(T first, Args... args) { /* ... */ }
递归与展开
理解参数包…的递归与展开很重要
#include <iostream>
// 基础情况:无参数
void printAll() {
std::cout << std::endl;
}
// 递归情况:至少一个参数
template <typename T, typename... Args>
void printAll(const T& first, const Args&... args) {
std::cout << first << " ";
printAll(args...); // 递归调用
}
int main() {
printAll(1, 2.5, "Hello", 'A'); // 输出:1 2.5 Hello A
return 0;
}
代码解析:定义参数T和参数包Args,第一次调用printAll是1给到first,其余给到参数包arges,注意这里的必须要有Args&,接下来打印first(1)和空格,接下来继续调用printAll,2.5给到first,其余给到参数包,以此类推知道完全打印完毕。
以此为基础,引出了折叠表达式:
#include <iostream>
template <typename... Args>
auto sum(Args... args) -> decltype((args + ...)) {
return (args + ...); // 左折叠
}
int main() {
std::cout << sum(1, 2, 3, 4) << std::endl; // 输出:10
std::cout << sum(1.5, 2.5, 3.0) << std::endl; // 输出:7
return 0;
}
代码解析:就是(args+…)这里代表的就是args1+args2…一次相加反复调用。decltype获取表达式类型
模板折叠
模板折叠分为左折叠右折叠,一元表达式折叠和二元表达式折叠
模板折叠许开发者直接对参数包应用操作符,而无需手动展开或递归处理参数。这不仅使代码更加简洁,还提高了可读性和可维护性。
左折叠和有折叠
#include <iostream>
// 左折叠:((args + ...) + init)的形式 — 从左向右折叠
template<typename... Args>
auto left_fold_sum(Args... args) -> decltype((... + args)) {
return (... + args); // 左折叠示例
}
// 右折叠:(args + ...) — 从右向左折叠
template<typename... Args>
auto right_fold_sum(Args... args) -> decltype((args + ...)) {
return (args + ...); // 右折叠示例
}
int main() {
std::cout << "Left fold sum (1,2,3,4): " << left_fold_sum(1, 2, 3, 4) << std::endl;
std::cout << "Right fold sum (1,2,3,4): " << right_fold_sum(1, 2, 3, 4) << std::endl;
return 0;
}
代码说明:
左折叠:(… + args) 展开时是 ( (…(args1 + args2) + args3) + …) 。
先计算最左边两个,加上第三个,依次向右加。
右折叠:(args + …) 展开时是 (args1 + (args2 + (args3 + …))) 。
先计算最右两个,然后向左包裹。
一元表达式折叠和二元表达式折叠
一元表达式折叠是对参数包各个成员进行一元表达式操作例如逻辑与或非
二元表达式折叠是对参数包各个成员进行加减乘除等操作
#include <iostream>
// 左一元折叠示例,检查所有参数是否都非零(逻辑与)
template<typename... Args>
bool all_nonzero(Args... args) {
return (... && args); // 所有参数通过 && 逐个折叠
}
int main() {
std::cout << std::boolalpha;
std::cout << all_nonzero(1, 2, 3) << std::endl; // 输出 true
std::cout << all_nonzero(1, 0, 3) << std::endl; // 输出 false
return 0;
}
#include <iostream>
template<typename... Args>
auto sum_with_init(Args... args) {
return (0 + ... + args); // 左二元折叠,初始值为 0
}
int main() {
std::cout << sum_with_init(1, 2, 3) << std::endl; // 输出 6
std::cout << sum_with_init() << std::endl; // 无参数时输出 0(无需重载)
return 0;
}
SFINAE的基本用法
SFINAE概述
SFINAE 是 “Substitution Failure Is Not An Error”(替换失败不是错误)的缩写,是C++模板编程中的一个重要概念。它允许编译器在模板实例化过程中,如果在替换模板参数时失败(即不满足某些条件),不会将其视为编译错误,而是继续寻找其他可能的模板或重载。这一机制为条件编译、类型特性检测、函数重载等提供了强大的支持。
工作原理
在模板实例化过程中,编译器会尝试将模板参数替换为具体类型。如果在替换过程中**出现不合法的表达式或类型,编译器不会报错,而是将该模板视为不可行的,继续尝试其他模板或重载。**这一特性允许开发者根据类型特性选择不同的模板实现
基本用法
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, void>::type
print_type(T value) {
std::cout << "Floating point type: " << value << std::endl;
}
int main() {
print_type(3.14); // 输出: Floating point type: 3.14
// print_type("Hello"); // 编译错误,没有匹配的函数
return 0;
}
利用std::is_floating_point判断T是不是浮点类型,如果是的话返回void
代码示例:检测类型是否有特定成员
假设我们需要实现一个函数,仅当类型 T 具有成员函数 foo 时才启用该函数。
#include <type_traits>
#include <iostream>
// 辅助类型,检测是否存在成员函数 foo
template <typename T>
class has_foo {
private:
typedef char yes[1];//定义 yes 为大小为1的字符数组类型
typedef char no[2];//定义 no 为大小为2的字符数组类型
template <typename U, void (U::*)()>//void无返回值,U::*指向U成员的指针,()接受参数
struct SFINAE {};声明
template <typename U>
static yes& test(SFINAE<U, &U::foo>*);//有foo返回yes
template <typename U>
static no& test(...);//没有foo返回no
public:
static constexpr bool value = sizeof(test<T>(0)) == sizeof(yes);//判断是不是yes
};
// 函数仅在 T 有 foo() 成员时启用
template <typename T>
typename std::enable_if<has_foo<T>::value, void>::type//当有foo成员的时候has_foo<T>::value这里为真,下面代码运行
call_foo(T& obj) {
obj.foo();
std::cout << "foo() called." << std::endl;
}
class WithFoo {
public:
void foo() { std::cout << "WithFoo::foo()" << std::endl; }
};
class WithoutFoo {};
int main() {
WithFoo wf;
call_foo(wf); // 输出: WithFoo::foo() \n foo() called.
// WithoutFoo wf2;
// call_foo(wf2); // 编译错误,没有匹配的函数
return 0;
}
结合模板特化与折叠表达式
实现一个通用的日志记录器Logger,能够处理任意数量和类型的参数,并根据不同的类型组合调整输出格式。具体需求包括:
- 对于普通类型,使用通用的打印格式。
- 对于指针类型,打印指针地址或指向的值。
- 对于std::string类型,使用专门的格式。
- 支持可变数量的参数,通过折叠表达式实现参数的逐一打印。
#include <iostream>
#include <string>
#include <type_traits>
// 通用类模板
template <typename T, typename Enable = void>//通用模板
class Logger {
public:
static void log(const T& value) {
std::cout << "General Logger: " << value << std::endl;
}
};
// 类模板偏特化:当 T 是指针类型
template <typename T>
class Logger<T, typename std::enable_if<std::is_pointer<T>::value>::type> {
public:
static void log(T value) {
if (value) {
std::cout << "Pointer Logger: " << *value << std::endl;
} else {
std::cout << "Pointer Logger: nullptr" << std::endl;
}
}
};
// 类模板全特化:当 T 是 std::string
template <>
class Logger<std::string> {
public:
static void log(const std::string& value) {
std::cout << "String Logger: \"" << value << "\"" << std::endl;
}
};
// 函数模板,用于递归调用 Logger::log
template <typename T>
void logOne(const T& value) {
Logger<T>::log(value);
}
// 使用模板折叠表达式实现多参数日志记录
template <typename... Args>
void logAll(const Args&... args) {
(logOne(args), ...); // 左折叠,调用 logOne 对每个参数进行日志记录
}
int main() {
int a = 10;
double b = 3.14;
std::string s = "Hello, World!";
int* ptr = &a;
double* pNull = nullptr;
// 使用 Logger 类模板进行特化打印
Logger<int>::log(a); // 输出:General Logger: 10
Logger<double*>::log(pNull); // 输出:Pointer Logger: nullptr
Logger<std::string>::log(s); // 输出:String Logger: "Hello, World!"
std::cout << "\nLogging multiple parameters:" << std::endl;
logAll(a, b, s, ptr, pNull);
/*
输出:
General Logger: 10
General Logger: 3.14
String Logger: "Hello, World!"
Pointer Logger: 10
Pointer Logger: nullptr
*/
return 0;
}
要点,编译器先找全特化,再找偏特化,再找通用
模板元编程
什么是模板元编程:模板元编程(Template Metaprogramming)是一种在编译期通过模板机制进行代码生成和计算的编程技术**。它利用编译器的模板实例化机制,在编译期间执行代码逻辑**,以提高程序的性能和灵活性。
实现阶乘
#include <iostream>
// 基础情况
template <int N>
struct Factorial {
static const int value = N * Factorial<N - 1>::value;
};
// 递归终止
template <>
struct Factorial<0> {
static const int value = 1;
};
int main() {
std::cout << "5! = " << Factorial<5>::value << std::endl; // 输出:5! = 120
std::cout << "0! = " << Factorial<0>::value << std::endl; // 输出:0! = 1
return 0;
}
关于静态成员变量
- 声明(Declaration):在类内部声明静态成员变量,告诉编译器该类包含这个静态成员。
- 定义(Definition):在类外部对静态成员变量进行定义,分配存储空间
特殊情况:static const 整数成员可以不在类外进行定义,但是缺点是编译器是指读这个变量
#include <iostream>
class A {
public:
static const int constValue = 42; // 1. 常量整型静态成员,类内初始化
static int normalValue; // 2. 普通静态成员变量,只声明
};
// 类外定义普通静态成员并初始化
int A::normalValue = 100;
int main() {
std::cout << "A::constValue = " << A::constValue << std::endl;
std::cout << "A::normalValue = " << A::normalValue << std::endl;
// 如果在类外不给normalValue定义并初始化,链接时报错
return 0;
}
template<int N>
struct Factorial{
static const int value = N * Factorial<N-1>::value;
};
// 类外定义
template<int N>
const int Factorial<N>::value;
inline 关键字
从 C++17 开始,引入了 内联变量(inline variables),使得在类内定义静态成员变量变得更加灵活:
○ 使用 inline 关键字,可以在类内对静态成员变量进行定义,无需在类外进行单独定义。
○ 这适用于 C++17 及更高版本
template<int N>
struct Factorial{
inline static const int value = N * Factorial<N-1>::value;
};
template<>
struct Factorial<0>{
inline static const int value = 1;
};
斐波那契数列
#include <iostream>
// 基础情况
template <int N>
struct Fibonacci {
static const long long value = Fibonacci<N - 1>::value + Fibonacci<N - 2>::value;
};
// 递归终止
template <>
struct Fibonacci<0> {
static const long long value = 0;
};
template <>
struct Fibonacci<1> {
static const long long value = 1;
};
int main() {
std::cout << "Fibonacci<10> = " << Fibonacci<10>::value << std::endl; // 输出:Fibonacci<10> = 55
std::cout << "Fibonacci<20> = " << Fibonacci<20>::value << std::endl; // 输出:Fibonacci<20> = 6765
return 0;
}
斐波那契的递归详细举例子
Fibonacci<2>::value
= Fibonacci<1>::value + Fibonacci<0>::value = 1 + 0 = 1
Fibonacci<3>::value
= Fibonacci<2>::value + Fibonacci<1>::value = 1 + 1 = 2
Fibonacci<4>::value
= Fibonacci<3>::value + Fibonacci<2>::value = 2 + 1 = 3
Fibonacci<5>::value
= Fibonacci<4>::value + Fibonacci<3>::value = 3 + 2 = 5
Fibonacci<6>::value
= Fibonacci<5>::value + Fibonacci<4>::value = 5 + 3 = 8
Fibonacci<7>::value
= Fibonacci<6>::value + Fibonacci<5>::value = 8 + 5 = 13
Fibonacci<8>::value
= Fibonacci<7>::value + Fibonacci<6>::value = 13 + 8 = 21
Fibonacci<9>::value
= Fibonacci<8>::value + Fibonacci<7>::value = 21 + 13 = 34
Fibonacci<10>::value
= Fibonacci<9>::value + Fibonacci<8>::value = 34 + 21 = 55
求和模板
// 基本递归模板
template <int... Ns>
struct Sum;
// 递归终止
template <>
struct Sum<> {
static const int value = 0;
};
// 递归定义
template <int N, int... Ns>
struct Sum<N, Ns...> {
static const int value = N + Sum<Ns...>::value;//拆分为N和后面的参数包
};
// 使用
int main() {
int result = Sum<1, 2, 3, 4, 5>::value; // 15
return 0;
}
代码可视化
Sum<1,2,3,4,5>::value
= 1 + Sum<2,3,4,5>::value
= 1 + (2 + Sum<3,4,5>::value)
= 1 + 2 + (3 + Sum<4,5>::value)
= 1 + 2 + 3 + (4 + Sum<5>::value)
= 1 + 2 + 3 + 4 + (5 + Sum<>::value)
= 1 + 2 + 3 + 4 + 5 + 0 // Sum<>基例情况
= 15
1596

被折叠的 条评论
为什么被折叠?



