C++模板学习笔记(三)

记录一下学习C++模板时的知识点

C++中.template的用法

template 关键字用于成员函数或嵌套模板名称,帮助编译器正确解析模板代码。在调用带有模板参数的模板函数时,编译器无法直接判断 < 是模板参数列表的开始还是小于号。此时必须用 .template 显式指明后续内容是模板。

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
struct Foo {
template<typename U>
void bar() {}
};

template<typename T>
void func(Foo<T>& f) {
f.bar<int>(); // 错误:编译器不知道 < 是模板还是小于号
f.template bar<int>(); // 正确:显式指明 bar 是模板
}

变量模板

在C++14之后,变量也可以模板参数化,称为变量模板。

1
2
3
4
5
template<typename T>
constexpr T pi{3.1415926535897932385};

std::cout << pi<double> << ’\n’;
std::cout << pi<float> << ’\n’;

模板作为模板参数的用法

比如自定义容器的底层存储数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <vector>
#include <list>

template <typename T, template <typename> class Container>
class Stack {
private:
Container<T> elements; // 可能是 vector<T> 或 list<T>
public:
void push(const T& val) { elements.push_back(val); }
};

int main() {
Stack<int, std::vector> s1; // 底层用 vector 存储
Stack<double, std::list> s2; // 底层用 list 存储
return 0;
}

std::vector  std::list 是模板,符合 template <typename> class Container 的约束。

这里需要注意的是,在C++17之前Container前的修饰符只能是class,C++17之后可以用typename。单个模板参数没有用到时可以省略。

其实也可以实例化之后再传参

1
2
3
4
5
6
7
template<typename T, typename Container = std::deque<T>>
class Stack {
private:
Container elems;
public:
...
}

std::enable_if的用法

std::enable_if 是 C++ 模板元编程中基于 SFINAE 的关键工具,用于 条件编译

定义

1
2
3
4
5
template <bool Condition, typename T = void>
struct enable_if;

// 当 Condition=true 时,enable_if<...>::type 存在(默认为 void)
// 当 Condition=false 时,enable_if<...>::type 不存在(触发 SFINAE)

限制模板编译的用法

模板参数条件编译

1
2
3
template<typename T, typename = std::enable_if_t<(sizeof(T) > 4)>>
void foo() {
}

如果sizeof(T)≤4这个模板函数就不会被编译,否则会被展开成

1
2
3
template<typename T, typename = void>
void foo() {
}

这里的typename = void 没有实际的意义

返回值条件编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 仅允许整数类型调用
template <typename T>
typename std::enable_if<std::is_integral<T>::value, void>::type
foo(T x) {
std::cout << "Integral: " << x << std::endl;
}

// 仅允许浮点类型调用
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, void>::type
foo(T x) {
std::cout << "Floating: " << x << std::endl;
}

foo(42); // OK, 调用整数版本
foo(3.14); // OK, 调用浮点版本
foo("hello"); // 编译错误(无匹配版本)

这里是在返回值位置上实现的条件编译,编译时对该条件进行判断,如果满足则进行编译,不满足则曲线编译。

模板特化控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 默认实现(禁用)
template <typename T, typename = void>
struct Bar {
static void print() { std::cout << "Default (disabled)\n"; }
};

// 仅对可迭代类型启用
template <typename T>
struct Bar<T, typename std::enable_if<has_iterator<T>::value>::type> {
static void print() { std::cout << "Iterable type\n"; }
};

Bar<int>::print(); // 输出 "Default (disabled)"
Bar<std::vector<int>>::print(); // 输出 "Iterable type"

参数按值传递还是按引用传递

函数模板参数优先使用按值传递,按值传递可以避免很多麻烦,但有些情况需要考虑按引用传递。

  • 对象不允许拷贝
  • 需要使用参数引用返回数据
  • 参数的左值右值属性需要转发(完美转发)
  • 需要减少拷贝优化性能

按值传递

即使是按值传递也可以使用移动语义减少拷贝

1
2
3
4
5
6
7
8
9
10
template<typename T>
void printV (T arg) {
...
}

printV(std::string("hi"));
printV(returnString());

std::string s = "hi";
printV(std::move(s));

按值传递有一个重要的特性是参数类型会退化,裸数组会退化成指针,const和volatile等限制符会被移除。

1
printV("hi"); // 有const char[3]类型转换成const char* 类型

按引用传递

按引用传递参数时,其类型不会退化(decay)。也就是说不会把裸数组转换为指针,也不会移除 const 和 volatile 等限制符。

但引用传递有多种情况需要考虑,使用上更加复杂。例如const常量引用和非常量引用,左值引用和右值引用。

const常量引用

const常量引用的问题较少,推荐一般情况下使用常量引用

1
2
3
4
5
6
std::string returnString();
std::string s = "hi";
printR(s); // no copy
printR(std::string("hi")); // no copy
printR(returnString()); // no copy
printR(std::move(s)); // no copy

由于调用参数被声明为 T const &,被推断出来的模板参数 T 的类型将不包含 const

非const常量引用

非const常量引用通常使用于需要用参数返回值的场景,由于非const修饰,因此不能传递临时变量和move变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <string>

template<typename T> void outR(T& arg) {} // 非常量引用

std::string returnString() {
return std::string("hello");
}

const std::string returnConstString() {
return std::string("hello");
}

int main() {
int x = 42;
const int cx = 42;

outR(x); // OK
outR(cx); // OK
outR("hello"); // OK,推导类型为const char[6]
outR(returnConstString()); // OK
outR(std::move(cx)); // OK
// outR(returnString()); // 错误: 临时变量不能传递给非常量引用
// outR(std::string("hello")); // 错误: 临时变量不能传递给非常量引用
// outR(42); // 错误: 字面数字常量不能传递给非常量引用
}

模板在推导时可能会推导出const属性,所以模板在修改参数值时可能会遇到错误,为了避免这个错误可以使用编译器检查static_assert或者条件编译std::enable_if

1
2
3
4
5
6
7
8
9
10
11
12
13
// static_assert
template<typename T>
void outR (T& arg) {
static_assert(!std::is_const<T>::value, "out parameter of foo<T>(T&)is const");

}

// std::enable_if
template<typename T,
typename = std::enable_if_t<!std::is_const<T>::value>
void outR (T& arg) {

}

完美转发引用传递

还有一种更复杂的引用叫完美转发,体现为保留参数在传递时的性质,如果实参是左值传递进来的形参也是左值,如果是右值形参也是右值,同时也能保留const的特性。

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
void passR (T&& arg) { // arg declared as forwarding reference

}

std::string s = "hi";
passR(s); // OK: T deduced as std::string& (also the type of arg)
passR(std::string("hi")); // OK: T deduced as std::string, arg is std::string&&
passR(returnString()); // OK: T deduced as std::string, arg is std::string&&
passR(std::move(s)); // OK: T deduced as std::string, arg is std::string&&
passR(arr); // OK: T deduced as int(&)[4] (also the type of arg)

完美转发也有缺点,如果在模板内部直接用 T 声明一个未初始化的局部变量,就会触发一个错误(引用对象在创建的时候必须被初始化)

引用包装器

将对象包装成引用包装器reference_wrapper),使其在模板或函数中按引用传递而非值传递,具体有以下两种:

  • std::ref(x)→包装成T&
  • std::cref(x)→包装成const T&

包装器可以显式的将引用传递给模板函数,避免按值传递。但其也有限制,它不能绑定临时对象,例如std::ref(42) ,需要目标函数/目标模板显式处理reference_wrapper。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <functional>
#include <iostream>

template<typename T>
void process(T arg) {
// 手动检查并解包 reference_wrapper
if constexpr (std::is_same_v<T, std::reference_wrapper<int>>) {
std::cout << "Reference: " << arg.get() << std::endl;
} else {
std::cout << "Value: " << arg << std::endl;
}
}

int main() {
int x = 42;
process(x); // 值传递
process(std::ref(x)); // 引用传递(显式处理)
}
作者

echo

发布于

2025-07-21

更新于

2025-07-21

许可协议

评论