本章关于内存管理新方法和与对象生命周期相关的优化机会,最后一个主题是类型感知的分配与释放函数(type-aware allocation and deallocation functions)。这是一种针对分配函数的全新方法,适用于用户代码希望以某种方式利用正在分配(以及最终构造)的对象的类型信息,来指导内存分配过程的场景。
第9章中已经看到过这类特性的一个方面:C++20 引入的“销毁删除(destroying delete)”机制。在这种机制中,类成员函数形式的 T::operator delete() 接收一个 T* 类型的参数,而不是传统的抽象的 void*。这使得该函数不仅要负责释放对象的底层存储空间,还要负责对象本身的最终化(finalization)。某些情况下,这种机制可以带来有趣的优化机会。
而对于 C++26,正在讨论的是一个全新的 operator new() 和 operator delete() 成员函数家族,以及一些独立函数。这些函数将接受一个 std::type_identity<T> 类型的对象作为第一个参数(其中 T 是某个具体类型),从而引导选择的操作符为该类型 T 执行特定的行为。
需要注意的是,这些类型感知的分配函数仅仅是用于内存分配的函数:不执行对象的构造,对应的内存释放函数也不执行对象的终结操作。
表达式 typename std::type_identity<T>::type 就等价于 T。看起来这非常简单。那么,在现代 C++ 编程中,这个特性扮演着什么样的角色呢?
这个特性 std::type_identity<T> 是在 C++20 中引入的,本质上是一个类型工具,通常用于在泛型函数中对模板参数的类型推导提供额外的控制。
例如,这样一个函数模板:
template <class T>
void f(T, T);
可以这样使用 f(3, 3),两个参数的类型相同;但你不能调用 f(3, 3.0),因为 int 和 double 是不同的类型。
然而,如果将其中一个参数的类型改为 std::type_identity_t<T>(即 std::type_identity<T>::type 的别名),就可以 f(3, 3.0)这样用。此时,T 会从另一个参数(类型为 T 的那个)推导出来,并将该类型用于另一个参数(即类型为 std::type_identity_t<T> 的那个)。这将导致两个参数都变成 int 或 double,具体取决于哪个参数是普通的 T 类型。
在类型感知分配函数中,使用 std::type_identity<T>(注意不是 std::type_identity_t<T>)而不是简单的 T 作为第一个参数的类型,其目的是为了明确表明我们正在使用的是这个特定的、专为类型感知而设计的 operator new() 重载版本,而不是调用了其他特殊形式的分配函数(比如第9章中提到的那些版本)。
(换句话说,这是为了清晰地表达:我们有意调用的是支持类型感知行为的分配函数重载。)
所以可以通过以下函数签名,为某个特定类 X 提供特供的分配函数:
#include <new>
#include <type_traits>
void* operator new(std::type_identity<X>, std::size_t n);
void operator delete(std::type_identity<X>, void* p);
当调用 new X 时,除非开发者采取措施加以阻止,否则编译器会优先选择这些特供的 operator new() 和 operator delete() 形式,因为它们被认为更加合适。
如果你有一个仅在满足 special_alloc_alg<T> 条件时才适用于类型 T 的特供分配算法,仍然可以通过以下形式的函数签名,为该类型 T 提供使用该特供算法的分配函数:
#include <new>
#include <type_traits>
template <class T> requires special_alloc_alg<T>
void* operator new(std::type_identity<T>, std::size_t n);
template <class T> requires special_alloc_alg<T>
void operator delete(std::type_identity<T>,
这为诸如第 10 章中所描述的优化提供了新的可能性。考虑这样一个简单的情况:为类型 X 和 Y 设计了一种高效的分配算法,但该算法并不适用于其他类,比如 Z:
#include <concepts>
#include <type_traits>
class X { /* ... */ };
class Y { /* ... */ };
class Z { /* ... */ };
template <class C>
concept cool_alloc_algorithm =
std::is_same_v<C, X> || std::is_same_v<C, Y>;
template <class T> requires cool_alloc_algorithm<T>
void* operator new(std::type_identity<T>, std::size_t n){
// 应用“酷”(自定义的)内存分配算法
}
template <class T> requires cool_alloc_algorithm<T>
void operator delete(std::type_identity<T>, void* p) {
// 应用“酷”(自定义的)内存分配算法
}
#include <memory>
int main() {
// 使用“酷”的分配算法
auto p = std::make_unique<X>();
// 使用标准的分配算法
auto q = std::make_unique<Z>();
} // 对q使用标准的分配算法析构
// 对p使用“酷”内存分配算法析构
类型感知的分配函数也可以是成员函数的重载形式,从而使得这些算法不仅适用于定义这些函数的类,也适用于其派生类。
以下示例灵感来源于提案中一个更复杂的例子,该提案描述的特性可在 https://wg21.link/p2719 找到:
class D0; // 前向的类声明
struct B {
// i)
template <class T>
void* operator new(std::type_identity<T>, std::size_t);
// ii)
void* operator new(std::type_identity<D0>, std::size_t);
};
// ...
如所示,i) 适用于 B 及其派生类,而 ii) 则适用于前向声明的类 D0 的特殊情况,并且只有当 D0 确实是 B 的派生类时才会使用。
延续这个例子,现在添加三个都从 B 派生的类,并且让 D2 添加 iii),即一个非类型感知的 operator new() 成员函数重载:
// ...
struct D0 : B { };
struct D1 : B { };
struct D2 : B {
// iii)
void *operator new(std::size_t);
};
// ...
鉴于这些重载,以下是一些调用 i)、ii) 和 iii) 的表达式示例:
// ...
void f() {
new B; // i) 此处 T 为 B
new D0; // ii)
new D1; // i) 此处 T 为 D1
new D2; // iii)
::new B; // 使用相应的全局 operator new
}
如果类型感知的分配函数纳入 C++ 标准,将提供新的方式来控制在不同情况下使用哪种内存分配算法,同时仍然让用户代码保有控制权 —— 如果全局的 operator new() 是更优选择,用户代码可以选择回退回到它,就像前面例子中 f() 函数的最后一行所示那样。
与 C++20 中的销毁删除(destroying delete)特性不同,后者同时执行对象的最终化和底层存储的释放,而类型感知的 operator new() 和 operator delete() 版本仅负责内存的分配与释放。在撰写本书时,尚未计划为销毁删除机制提供类型感知版本。