CppCon 2020 学习: B2B:C++Templates Part1
·
什么是泛型编程(Generic Programming)
- 泛型编程是一种以最通用且合理的方式实现算法和数据结构的方法。
- 这些算法是用“类型占位符”来写的,具体类型会在以后指定。
- “泛型编程”这一术语最初由David Musser和Alexander Stepanov提出。
- 泛型编程帮助我们减少代码重复和编程工作量,同时提高代码的可重用性和灵活性。
模板(Templates) - 模板是一种供编译器使用的“模式”或“蓝图”。
- 我们可以用不同的类型或不同的值来实例化(生成)模板。
- 每当为一个新的类型或值实例化模板时,编译器会生成对应的新代码,这个模板里的内容会用给定的模板参数来替换。
- 模板大大减少了程序员的工作量,我们不用因为类型稍有不同就重复实现多次相似的函数或类。
- 模板有不同的种类:
- 函数模板
- 类模板
- 变量模板(从C++14开始支持)
- 模板定义时总是以关键字
template开头。
不同类型的模板参数 - 模板参数有三种不同类型:
- 类型参数(Type parameter)
- 用于指定具体的类型,比如
int、char,甚至是一个类。 - 类型参数是最常见的模板参数类型。
- 用于指定具体的类型,比如
- 非类型参数(Non-type parameter)
- 通常是像数字3这样的值。
- 不包括浮点数和字符串(C风格字符数组)。
- 从C++20开始,浮点数和字符串等也能作为非类型参数使用,但有一些小限制。
- 模板模板参数(Template template parameter)
- 当我们要把一个模板作为参数传递给另一个模板时,需要用这种参数类型。
模板组成部分应用示例
下面是一段声明和使用模板的代码:
- 当我们要把一个模板作为参数传递给另一个模板时,需要用这种参数类型。
- 类型参数(Type parameter)
#include <cstddef>
#include <iostream>
// 1. 模板头部声明
template <typename T, size_t N> // 模板参数列表,T 是类型参数,N 是非类型模板参数(NTTP)
constexpr auto Size(const T (&)[N]) // 函数模板,参数是类型为 T 且大小为 N 的数组的引用
{
return N; // 返回数组大小,使用了非类型模板参数 N
}
int main() {
char buffer[16]{}; // 定义一个大小为16的字符数组
// 推荐使用基于范围的 for 循环!
for (size_t i = 0; i < Size(buffer); ++i) {
// 这里对 buffer 进行某些操作
}
std::cout << Size(buffer) << std::endl;
}
- 模板头部声明了两个模板参数:
typename T—— 类型参数size_t N—— 非类型模板参数(数组大小)
- 函数
Size接受一个类型为T且大小为N的数组引用作为参数,返回数组的大小N。 - 在
Main函数中,定义了一个char类型的数组buffer,大小是16。 - 使用
Size(buffer)得到数组大小用于循环遍历数组元素。
这段代码展示了模板如何结合类型参数和非类型参数来写通用的函数,自动获取数组大小,非常实用且简洁。
函数模板(Function Templates) - 通过函数模板,我们可以一次性实现针对不同类型的统一功能,由编译器来完成具体类型的替换和生成代码。
- 模板参数可以是类型或值,但有一定限制:
- 类型参数用关键字
typename或class来声明。 - 模板参数名紧跟在
typename或class后面。 - 通常第一个模板参数命名为
T,第二个为U。
- 类型参数用关键字
- 在函数内部,我们使用模板参数名代替具体的类型。
示例代码解释:
#include <cstdio>
template <typename T> // 声明模板,模板参数为类型 T
T min(const T& a, const T& b) // 函数 min,比较两个相同类型的参数,返回较小值
{
return (a < b) ? a : b;
}
int main() {
const int a = 2;
const int b = 1;
printf("%d\n", min(a, b)); // 调用模板函数,编译器推断 T 为 int,输出较小值 1
}
这段代码定义了一个通用的 min 函数模板,可以比较任意类型的两个值(前提是该类型支持 < 比较运算符),返回其中较小的一个。在 Main 函数中,调用了 min,传入了两个 int 类型的值,输出结果是较小的那个。
函数模板的特化(Function Template Specialization)
- 特化 是为函数模板的某种具体参数组合,提供专门实现的过程。
- 默认的模板版本适用于所有类型,特化版本用于处理特定类型,以覆盖默认实现。
示例说明:
#include <cmath>
#include <cstdio>
template <typename T>
bool equal(const T& a, const T& b) {
return a == b; // 通用版本,使用 == 比较
}
// 针对 double 类型的特化版本
template <>
bool equal(const double& a, const double& b) {
// 由于浮点数精度问题,比较时允许有一个很小的误差
return std::abs(a - b) < 0.00001;
}
int main() {
int a = 2;
int b = 1;
printf("%d\n", equal(a, b)); // 调用通用版本,输出 0 (false)
double d = 3.0;
double f = 4.0;
printf("%d\n", equal(d, f)); // 调用 double 特化版本,输出 0 (false)
}
- 第一段模板是通用版本,比较任意类型的两个值是否相等。
- 第二段模板是对
double类型的特化,实现了考虑浮点误差的比较方法。 - 调用
equal函数时,编译器根据参数类型选择合适的实现。
类模板(Class Templates) - 类模板和函数模板类似,都是用关键字
template引入的。 - 大多数函数模板的规则,同样适用于类模板。
- 类的方法既可以在类内部实现,也可以在类外部实现。
- 如果在类外部实现方法,必须在方法实现前加上模板声明(
template<...>)并带上类模板参数。 - 每一次类模板的实例化(即为模板参数赋具体值)都会生成一个新的类型。
- 这里:
- A 代表类型模板参数(type parameter)
- B 代表非类型模板参数(NTTP, non-type template parameter)
示例代码解析:
#include <cstddef>
#include <memory>
template <typename T, size_t SIZE> // 类型参数 T + 非类型参数 SIZE
struct Array {
T* data();
const T* data() const {
return std::addressof(mData[0]); // 返回数组首元素地址
}
constexpr size_t size() const { return SIZE; } // 返回数组大小(编译期常量)
T* begin() { return data(); } // 迭代起始位置
T* end() { return data() + size(); } // 迭代结束位置
T& operator[](size_t idx) { return mData[idx]; } // 下标访问运算符
T mData[SIZE]; // 固定大小数组
};
// 类外实现成员函数
template <typename T, size_t SIZE>
T* Array<T, SIZE>::data() {
return std::addressof(mData[0]);
}
int main() {
Array<int, 2> ai{3, 5}; // 实例化 Array<int, 2>
Array<double, 2> ad{2.0}; // 实例化 Array<double, 2>
}
要点总结:
- 类模板 = 模板参数 + 类定义。
- 非类型模板参数(NTTP)可用来定义编译期常量(如数组大小)。
- 在类外定义成员函数时,必须完整写出模板参数列表和类名,例如:
template<typename T, size_t SIZE> T* Array<T, SIZE>::data() { ... } - 每一次实例化都会生成一个新的类型,
Array<int, 2>与Array<double, 2>是完全不同的类型。
如果你愿意,我可以帮你画一个类模板实例化的示意图,让你直观看到它是如何从模板变成具体类型的。这样你对它的编译过程会理解得更透彻。
类模板:实例化(Instantiation)
- 类模板的实例化方式与函数模板类似,但有一个重要区别:
- 类模板不能自动推导其模板参数。
- 在实例化类模板时,必须显式地指定所有模板参数。
- 例外:从 C++17 开始,引入了类模板实参推导(Class Template Argument Deduction, CTAD),允许编译器在某些情况下根据构造函数的参数类型自动推导模板参数。
示例(C++17 前)
Array<int, 5> arr; // 必须显式写出 <int, 5>
示例(C++17 起,CTAD)
Array arr{1, 2, 3, 4, 5}; // 编译器自动推导出 Array<int, 5>
如果你愿意,我可以帮你把CTAD 的工作原理和限制条件画成一张图,你会更容易理解它在编译期的推导过程。
类模板中的方法模板(Method Templates)
- 类模板中的方法本身也可以是模板,这称为方法模板。
- 方法模板既可以在类内部定义,也可以在类外部定义。
- 拷贝构造函数和析构函数不能是模板。
示例解析:
template <typename T>
class Foo {
public:
Foo(const T& x) // 构造函数
: mX{x} {}
// 方法模板:operator= 是一个模板方法
template <typename U>
Foo<T>& operator=(const U& u) {
mX = static_cast<T>(u); // 将 U 类型转换为 T 类型
return *this;
}
private:
T mX; // 成员变量
};
int main() {
Foo<int> fi{3}; // 类模板实例化,T = int
fi = 2.5; // 调用方法模板 operator=<double>
}
要点总结:
- 类模板的模板方法允许该方法在调用时拥有自己的独立模板参数,与类模板参数
T不冲突。 - 在上例中:
- 类模板
Foo<T>的T决定了类的主类型。 - 方法模板
operator=<U>的U决定了赋值时接受的参数类型。
- 类模板
fi = 2.5;时:T已经是int(类实例化时确定的)。U被推导为double(调用赋值运算符时推导的)。- 赋值运算符将
double转换为int并存储。
类模板:继承(Inheritance)
- 类模板和普通类之间,可以互相继承,组合方式没有限制。
- 当类模板作为派生类继承基类模板时,有一个限制:
- 在派生类中,基类的方法和成员不会自动直接可用(名字查找规则导致)。
解决方法有三种:
- 在派生类中,基类的方法和成员不会自动直接可用(名字查找规则导致)。
- 使用
this指针限定this->Func(); - 使用基类的完整限定名
Foo<T>::Func(); - 直接调用基类方法(不推荐,因为可能引发模板依赖查找问题)
示例解析:
template <typename T>
class Foo {
public:
void Func() {}
};
template <typename T>
class Bar : public Foo<T> // Bar<T> 继承自 Foo<T>
{
public:
void BarFunc() {
// Func(); // 编译错误,找不到名字
this->Func(); // 方法 1
Foo<T>::Func(); // 方法 2
}
};
int main() {
Bar<int> b{};
b.BarFunc(); // 调用 BarFunc 内部的基类方法
}
要点总结:
- 模板派生类访问基类模板成员时,编译器不会自动在基类模板中查找未限定名字(这是“依赖名查找”规则)。
- 必须通过
this->或基类名<模板参数>::来告诉编译器去基类模板中找。 - 这与普通类继承不同,是模板特有的规则。
如果你需要,我可以帮你画一张**依赖名查找(Dependent Name Lookup)**的流程图,让你直观看出为什么Func()在模板继承中不能直接调用。这样会非常清楚。
别名模板(Alias Templates) - 别名模板允许你为模板创建别名(同义名)。
- 使用别名模板,可以部分特化已有模板(例如固定某些模板参数,保留其他参数可变)。
- 别名模板本身不能再进行进一步特化。
示例解析:
#include <array>
#include <cstddef>
// 定义别名模板 CharArray,将 std::array<char, N> 简化命名
template <size_t N>
using CharArray = std::array<char, N>;
int main() {
CharArray<24> ar; // 等价于 std::array<char, 24>
}
要点:
using关键字可以用来为类型模板创建别名,比typedef更强大,因为它支持模板参数。- 在上例中:
- 原本要写
std::array<char, 24>,现在可以简写为CharArray<24>。 - 这种方式等于“预先固定了模板的第一个类型参数为
char”,保留N可变。
- 原本要写
- 别名模板的作用主要是代码简化和可读性提升。
高效使用模板的指南
- 模板会为我们自动生成代码。
- 这种生成过程,就像是把实现代码复制粘贴一份,然后把类型或值替换成对应的模板参数。
- 根据编译器和优化器的不同,这种方式可能会导致可执行文件体积变大。
- 有时候我们会忽视这个问题,这种现象通常被称为 代码膨胀(code bloat)。
- 不过,这个问题是可以由我们控制的(例如通过减少不必要的模板实例化、抽象公共代码等方法)。
高效使用模板的指南 — 示例 - 典型模式:传递数据指针和数据长度
- 这种模式常见于 C 语言 API。
- 缺点:
- 容易出错(比如传错长度或指针)。
- 代码冗长,写和读都需要额外的参数。
示例代码说明:
#include <cstddef> // size_t
#include <cstdio> // printf
#include <cstring> // memcpy 模拟数据读取
// 模拟一个写函数(实际中可能是系统 API,比如 send/write)
bool write(const char* data, size_t size) {
printf("Writing %zu bytes: ", size);
for (size_t i = 0; i < size && i < 10; ++i) { // 只打印前10个字节
printf("%02X ", static_cast<unsigned char>(data[i]));
}
if (size > 10) printf("...");
printf("\n");
return true;
}
bool Send(const char* data, size_t size) {
if (!data) { // 检查空指针
return false;
}
return write(data, size); // 发送数据
}
void Read(char* data, size_t size) {
if (!data) { // 检查空指针
return;
}
// 模拟读取数据:用固定值填充缓冲区
std::memset(data, 0x41, size); // 'A'
}
int main() {
char buffer[1'024]{}; // 定义 1024 字节缓冲区
Read(buffer, sizeof(buffer)); // 必须手动传缓冲区大小
Send(buffer, sizeof(buffer)); // 这里容易传错
char buffer2[2'048]{}; // 定义 2048 字节缓冲区
// 错误:传错了参数,把 buffer 当成 sizeof 的参数
Read(buffer, sizeof(buffer2));
Send(buffer, sizeof(buffer2));
return 0;
}
要点:
- 在这种 C 风格 API 中,指针和长度是分离的,调用者必须手动保证它们一致。
- 一旦手动传错(像例子中最后两次调用那样),就可能导致:
- 数据越界
- 数据丢失
- 程序崩溃
- 模板可以帮助自动获取长度,避免手动传值造成的错误,从而提升代码安全性和可维护性。
高效使用模板的指南 — 改进示例 - 原始做法(传指针 + 长度)是典型的 C API 风格:
- 容易出错(手动传错长度)。
- 调用时写起来繁琐(多一个
size参数)。
- 改进做法:使用
std::array- 优点:
std::array自带大小信息,不需要手动传长度。 - 缺点:大小必须是编译期常量,且类型固定。
- 进一步改进:用 非类型模板参数(NTTP, Non-Type Template Parameter) 来让
Read和Send支持不同大小的数组,并在编译期确定长度。
示例解析:
- 优点:
#include <cstddef> // size_t
#include <array>
#include <iostream> // 模拟输出
// 模拟底层 write 函数(实际中可能是系统调用)
bool write(const char* data, size_t size) {
std::cout << "写入数据大小: " << size << " 字节\n";
return true; // 假设写入成功
}
template <size_t N>
bool Send(const std::array<char, N>& data) {
return write(data.data(), data.size()); // data.data() 返回指针,data.size() 返回大小
}
template <size_t N>
void Read(std::array<char, N>& data) {
// 模拟填充缓冲区
for (size_t i = 0; i < data.size(); ++i) {
data[i] = 'A' + (i % 26);
}
}
int main() {
std::array<char, 1'024> buffer{}; // 定义一个 1024 字节的 std::array
Read(buffer); // 编译器推导 N=1024
Send(buffer); // 无需手动传长度
std::array<char, 2'048> buffer2{}; // 定义一个 2048 字节的 std::array
Read(buffer2); // 编译器推导 N=2048
Send(buffer2); // 同样无需手动传长度
}
要点总结:
- 通过 模板的非类型参数
N,可以让函数根据数组大小自动实例化,不必手动传长度。 - 避免了 C 风格 API 中容易传错长度的风险。
- 编译器在编译期就能确定大小,并进行类型安全检查。
高效使用模板的指南 — 具体示例
- 传递数据和长度的模式:
- 这是典型的 C 风格 API 写法。
- 容易出错,因为调用时必须手动保证指针和长度匹配。
- 写和读时都要多写参数,代码冗长。
- 使用
std::array改进:std::array自带大小信息,调用时不需要再传长度,减少错误。- 缺点是数组大小必须固定且相同。
- 进一步改进:
- 将
Read和Send定义为模板,模板参数为数组大小(非类型模板参数,NTTP),支持不同大小的数组。 - 这样就能同时支持不同大小的
std::array,且编译期确定大小。
- 将
- 需要注意的是:
- 这种模板方式可能会导致 代码膨胀(code bloat),因为每种大小都会实例化一份代码。
示例代码:
- 这种模板方式可能会导致 代码膨胀(code bloat),因为每种大小都会实例化一份代码。
#include <array>
#include <cstddef>
#include <iostream>
// 模拟底层写函数
bool write(const char* data, size_t size) {
std::cout << "写入数据大小: " << size << " 字节\n";
return true;
}
template<size_t N>
bool Send(const std::array<char, N>& data) {
return write(data.data(), data.size());
}
template<size_t N>
void Read(std::array<char, N>& data) {
// 模拟填充缓冲区,填充字符 'A'
for (size_t i = 0; i < data.size(); ++i) {
data[i] = 'A' + (i % 26);
}
}
int main() {
std::array<char, 1'024> buffer{};
Read(buffer);
Send(buffer);
std::array<char, 2'048> buffer2{};
Read(buffer2);
Send(buffer2);
return 0;
}
总结:
利用模板和 std::array 的大小作为模板参数,可以让接口更加安全和简洁,减少人为错误,但要注意可能增加生成代码的体积。
如果需要,我可以帮你讲讲如何平衡模板代码膨胀和灵活性的策略。
高效使用模板的指南 — 改进示例(使用 span)
- 传递数据和长度的模式:
- 典型 C API 风格,容易出错且调用繁琐。
- 用
std::array改进时,数组大小必须固定且相同。 - 使用模板加非类型模板参数(NTTP)虽然灵活,但可能导致代码膨胀。
- 更好的方式是 抽象数组大小,让接口更加通用,降低代码冗余和复杂度。
- 例如使用
span(C++20 标准或第三方实现),它可以同时持有 C 风格数组和std::array,只保存数据指针和大小。 span支持范围for,使代码更加简洁安全。
示例代码逻辑说明:
#include <iostream>
#include <array>
#include <span> // C++20 的 std::span
// 模拟底层写函数
bool write(const char* data, size_t size) {
std::cout << "写入数据大小: " << size << " 字节\n";
// 打印前几个字节内容示意
for (size_t i = 0; i < size && i < 10; ++i) {
std::cout << static_cast<int>(static_cast<unsigned char>(data[i])) << ' ';
}
if (size > 10) std::cout << "...";
std::cout << '\n';
return true;
}
bool Send(std::span<char> data) {
return write(data.data(), data.size());
}
void Read(std::span<char> data) {
int i = 1;
for (auto& c : data) {
c = static_cast<char>(i++);
}
}
int main() {
std::array<char, 1024> buffer{};
Read(buffer); // std::array 自动转换为 std::span
Send(buffer);
char buffer2[2048]{};
Read(buffer2); // C 风格数组自动转换为 std::span
Send(buffer2);
return 0;
}
总结:
- 使用
span可以统一处理多种数组类型,避免手动传长度。 - 代码简洁,易读且安全。
- 降低了模板实例化导致的代码膨胀风险。
高效使用模板的指南 — 示例说明 - 传递数据和长度是典型的 C API 风格,容易出错,且写和读都要多写参数。
- 使用
std::array改进,但其大小必须固定且相同。 - 利用非类型模板参数(NTTP)做模板化的
Read和Send,可以支持不同大小的数组,但可能带来代码膨胀(code bloat)。 - 更好的方式是抽象掉数组大小,例如使用
span,它可以容纳 C 风格数组和std::array,且支持范围for,代码简洁且开销小。 - C++20 标准库自带
std::span,且有单头文件版本可用。
自定义span类示例(C++20 单头文件版本)解析:
template<typename T>
class span {
public:
constexpr span() = default; // 默认构造函数
constexpr span(T* start, const size_t len)
: data_{start}, length{len} { } // 指针+长度构造
template<size_t N>
constexpr span(T (&arr)[N]) // 接受数组引用,推导大小
: span(arr, N) { }
template<size_t N>
constexpr span(const T (&arr)[N]) // 常量数组引用版本
: span(arr, N) { }
template<size_t N, class AT = std::remove_const_t<T>>
constexpr span(std::array<AT, N>& arr) // 接受 std::array,自动获取数据和大小
: span(arr.data(), arr.size()) { }
constexpr size_t size() const { return length; }
T* data() const { return data_; }
bool empty() const { return nullptr != data_; }
T* begin() const { return data_; }
T* end() const { return data_ + length; }
private:
T* data_;
size_t length;
};
重点总结:
span是一个轻量的视图类,持有指针和长度,不拥有数据。- 支持从原生数组、常量数组和
std::array构造,方便统一接口。 - 支持范围基的
for迭代器(begin(),end()),代码清晰简洁。 - 这种设计清理了接口,避免了传指针和大小的繁琐与易错。
#include <cstddef>
#include <array>
#include <iostream>
#include <type_traits>
// 自定义 span 类模板(简化版)
template<typename T>
class span {
public:
constexpr span() = default;
constexpr span(T* start, const size_t len)
: data_{start}, length{len} {}
template<size_t N>
constexpr span(T (&arr)[N])
: span(arr, N) {}
template<size_t N>
constexpr span(const T (&arr)[N])
: span(arr, N) {}
template<size_t N, class AT = std::remove_const_t<T>>
constexpr span(std::array<AT, N>& arr)
: span(arr.data(), arr.size()) {}
constexpr size_t size() const { return length; }
T* data() const { return data_; }
bool empty() const { return data_ == nullptr; }
T* begin() const { return data_; }
T* end() const { return data_ + length; }
private:
T* data_ = nullptr;
size_t length = 0;
};
// 模拟底层写函数
bool write(const char* data, size_t size) {
std::cout << "写入数据大小: " << size << " 字节\n";
for (size_t i = 0; i < size && i < 10; ++i) {
std::cout << static_cast<int>(static_cast<unsigned char>(data[i])) << ' ';
}
if (size > 10) std::cout << "...";
std::cout << '\n';
return true;
}
// 使用自定义 span 的 Send
bool Send(span<char> data) {
return write(data.data(), data.size());
}
// 使用自定义 span 的 Read
void Read(span<char> data) {
int i = 1;
for (auto& c : data) {
c = static_cast<char>(i++);
}
}
int main() {
std::array<char, 1024> buffer{};
Read(buffer); // 自动构造 span
Send(buffer);
char buffer2[2048]{};
Read(buffer2); // 自动构造 span
Send(buffer2);
return 0;
}
高效使用模板的指南
- 关于类模板的指南:
- 把所有实例化都相同的代码放到基类中,避免重复生成代码。
- 如果要存储额外的类型或值,使用成员变量(权衡存储开销),比把它作为模板参数传递更好(减少模板实例化数量)。
- 关于函数模板的指南:
- 尽量设计函数模板作为对外接口(API),但把真正的工作转发给非模板函数完成。
- 这样能为用户提供类型安全且简单的接口,同时内部使用非模板函数减少代码膨胀和复杂度。
思考类型
- 我们通常习惯在运行时去思考和处理值。
- 类型是在编译时已知的。
- 我们可以在编译时对类型进行检查和修改。
思考类型(Thinking in types) - 我们通常在运行时思考值的计算过程。
- 类型信息是在编译时已知的。
- 我们可以在编译阶段对类型进行检查和修改。
示例:限制数组不能是指针 - 标准库中有一个类型特征
is_pointer,用来判断某个类型是否是指针类型。 - 这里给出了一个简化版的
is_pointer实现,类似于标准库type_traits的做法。
代码解析: - A
integral_constant:
一个模板结构体,用于在编译期存储一个值(比如布尔值)。template<class T, T v> struct integral_constant { static constexpr T value = v; }; - B 类型别名,方便使用:
using true_type = integral_constant<bool, true>; using false_type = integral_constant<bool, false>; - C 默认模板
is_pointer,针对所有类型默认是false_type(即非指针类型):template<class T> struct is_pointer : false_type {}; - D 模板特化,当类型是指针
T*时,is_pointer继承自true_type:template<class T> struct is_pointer<T*> : true_type {}; - E 测试用
static_assert(编译期断言),验证类型判断是否正确:static_assert(is_pointer<int*>::value); static_assert(!is_pointer<int>::value);
总结:
利用编译期的类型特征机制,可以写出在编译阶段判断类型属性的代码,从而辅助模板编程和类型安全设计。
思考类型(Thinking in types)
- 我们通常关注的是运行时的值,值是在程序运行时计算的。
- 类型是在编译时就确定的。
- 因此,我们可以在编译阶段对类型进行检查和修改。
示例:限制模板参数类型不能是指针 - 这里定义了一个模板结构体
Array,带两个模板参数:元素类型T和大小SIZE。 - 通过
static_assert使用标准库的类型特征std::is_pointer,在编译时检查T不是指针类型。
代码解析:
template<typename T, size_t SIZE>
struct Array
{
// A 编译期断言,确保 T 不是指针类型
static_assert(not std::is_pointer<T>::value);
T* data() { return std::addressof(mData[0]); }
const T* data() const { return std::addressof(mData[0]); }
constexpr size_t size() const { return SIZE; }
T* begin() { return data(); }
T* end() { return data() + size(); }
T& operator[](size_t idx) { return mData[idx]; }
T mData[SIZE];
};
static_assert(not std::is_pointer<T>::value);
如果传入的T是指针类型,则编译错误,防止使用Array<int*, N>这样的非法实例化。
测试用例:
void Main()
{
int x{22};
// Array<int*, 2> invalid{&x}; // B 这行代码将导致编译失败,因为 int* 是指针类型
Array<int, 2> valid{x}; // 合法,用 int 类型实例化
}
总结:
- 利用编译时类型特征(
std::is_pointer)和断言(static_assert)可以在模板实例化时保证类型安全,避免非法类型用作模板参数。 - 这提升了模板代码的健壮性和易维护性。
(Thinking in types)
- 我们通常思考的是运行时的值,而类型是在编译时就已知的。
- 编译时可以检查和修改类型,从而提高类型安全。
- 目标:限制
Array模板的元素类型T不能是指针。
用 C++20 Concepts 实现类型限制
- 利用 C++20 的
requires约束,实现编译时限制模板参数T不是指针类型。 - 代码示例:
#include <cstddef>
#include <type_traits> // std::is_pointer_v
#include <memory> // std::addressof
#include <iostream>
template <typename T, size_t SIZE>
requires(not std::is_pointer_v<T>) // 限制 T 不能是指针
struct Array {
// 支持列表初始化
Array(std::initializer_list<T> init) {
size_t i = 0;
for (auto&& val : init) {
if (i < SIZE) mData[i++] = val;
}
// 如果 init.size() < SIZE,剩余元素默认初始化为 T 的默认值
for (; i < SIZE; ++i) mData[i] = T{};
}
T* data() { return std::addressof(mData[0]); }
const T* data() const { return std::addressof(mData[0]); }
constexpr size_t size() const { return SIZE; }
T* begin() { return data(); }
T* end() { return data() + size(); }
T& operator[](size_t idx) { return mData[idx]; }
const T& operator[](size_t idx) const { return mData[idx]; }
T mData[SIZE];
};
int main() {
int x{22};
// Array<int*, 2> invalid{&x}; // 编译失败,禁用指针类型
Array<int, 2> valid{x, 33}; // 合法,列表初始化
for (size_t i = 0; i < valid.size(); ++i) {
std::cout << valid[i] << " ";
}
std::cout << "\n";
return 0;
}
constexpr if(C++17)
constexpr if是constexpr的扩展。- 条件和所有分支在编译时求值,只有为
true的分支被保留,其它分支不会被编译。 - 可以用来做编译时分支判断,实现模板内的条件行为。
示例:用 constexpr if 实现指针与非指针值的统一访问
#include <type_traits> // std::is_pointer_v
#include <cassert>
template <typename T>
auto getValue(T t) {
if constexpr (std::is_pointer_v<T>) {
assert(t != nullptr);
return *t; // 如果是指针,解引用返回值
} else {
return t; // 否则直接返回
}
}
int main() {
int i = 4;
int* ip = &i;
auto iv = getValue(i); // iv 是 int,值为 4
auto ipv = getValue(ip); // ipv 是 int,值为 4(解引用指针)
auto itv = getValue(43); // itv 是 int,值为 43
}
示例:用 constexpr if 实现类型到字符串的转换
#include <string>
#include <type_traits>
#include <iostream>
template<typename T>
std::string str(T t)
{
if constexpr(std::is_convertible_v<T, std::string>) {
return t; // 如果能转成 std::string,直接返回
} else {
return std::to_string(t); // 否则调用 std::to_string 转换
}
}
int main()
{
auto s = str(std::string{"42"}); // 返回字符串 "42"
auto i = str(42); // 返回字符串 "42"(数字转字符串)
std::cout << "s = " << s << "\n";
std::cout << "i = " << i << "\n";
return 0;
}
总结
- C++20 Concepts 可以在模板参数层面进行类型约束,使代码更安全、易读。
- C++17 的
constexpr if允许编译时分支,编写更灵活和高效的模板代码。 - 结合类型特征(如
std::is_pointer_v、std::is_convertible_v)可以在编译时实现复杂的类型判断和分支逻辑。
更多推荐



所有评论(0)