C++使用TaggedPointer的方式实现高效编译时多态
核心Tagged Pointer
特性 | 传统虚函数 (Virtual Functions) | Tagged Pointer (本例) |
多态类型 | 运行时多态 (Runtime Polymorphism) | 编译时多态 (Compile-Time Polymorphism) |
核心机制 | 虚函数表 (vtable) | 类型标签 (Type Tag) + Dispatch |
内存开销 | 每个对象 1 个 vptr (虚函数表指针,通常 8 字节)。 | 零开销。 类本身就是 8 字节(一个指针大小)。它指向的对象 ( 等) 也不需要 vptr。 |
性能开销 | 间接调用 (Indirect Call): 。这涉及两次内存查找,且难以被编译器内联 (inline)。 | 直接调用 (Direct Call): 内部的 + + 直接函数调用。这个最终的调用可以被内联,速度极快。 |
灵活性 | 开放集 (Open Set):你可以随时在代码库的任何地方继承基类,创建新的子类,系统无需修改即可工作。 | 封闭集 (Closed Set):所有可能的类型 ( , 等) 必须在 定义时全部列出。你不能在别处添加一个新类型而不修改 的定义。 |
数据位置 | vptr 位于对象实例内部。 | Tag 位于指针本身 (的包装器) 内部。被指向的对象是 "干净" 的。 |

- 窃取指针的高位,在 64 位系统上,一个指针虽然有 64 位,但现代 CPU(如 x86-64)的虚拟地址空间实际上并未使用全部 64 位。它们通常只使用低 48 位或 57 位。这意味着指针的高位(top bits)总是 0(或根据特定约定进行符号扩展)。
- 存储 "Tag" (标签):
TaggedPointer类利用了这些未使用的高位。它将一个 64 位的uint64_t bits成员变量一分为二:
- 低 57 位 (ptrMask): 用于存储实际的指针地址。
- 高 7 位 (tagMask): 用于存储一个 "Tag" 或 "Type ID",这是一个小整数,用来标识指针真正指向的数据类型。
- 代码中定义了指针的一个mask这个分割点由
tagShift = 57定义: ptrMask(指针掩码): 覆盖 0 到 56 位。tagMask(标签掩码): 覆盖 57 到 63 位 (共 7 位,可以表示种不同类型)。
如果 static 成员被 constexpr 修饰,且在类内通过「编译期可计算的表达式」初始化,那么这个类内初始化就是完整的「定义」,无需在类外(.cpp)重复定义。
template<typename... Ts>
struct TypePack
{static constexpr size_t count = sizeof...(Ts);
};
template<typename... Ts>
class TaggedPointer
{
public:using Types = TypePack<Ts...>;
private:static_assert(sizeof(uintptr_t) <= sizeof(uint64_t),"Expected pointer size to be <= 64 bits");static constexpr int tagShift = 57;static constexpr int tagBits = 64 - tagShift;static constexpr uint64_t tagMask = ((1ull << tagBits) - 1) << tagShift;static constexpr uint64_t ptrMask = ~tagMask;uint64_t bits = 0;
};可以看到上面的代码,就是将高7位定义为了类型,低57为地址。
如何使用的呢?我们后续肯定是定义这个TaggedPointer的派生类为对应的工厂去操作不同的指针实现多态。
这个主要是我们实际上就是去根据下面的方式定义这个“基类”,我们初始化这个Spectrum指针,继承TaggedPointer,对应的就会继承他的所有的字段。这里的字段中就会保存一个地址,在bits的低位,然后高位可以得到对应的类型。
这样有什么好处?
我们传统的分配内存空间需要派生类copy一个虚表,然后在对象内存空间中还需要分配一个虚表指针,然后在拷贝数据字段。同时我们调用虚函数还需要去寻址。上面的表格指出了性能和内存的开销。
我们下面这种方式只需要定义一个Spectrum的指针或者值,然后得到对应的“派生类”的对象地址,然后用dispatch的方式根据地址对应的对象调用对应的方法就可以了。
class Spectrum : public TaggedPointer<ConstantSpectrum, DenselySampledSpectrum,PiecewiseLinearSpectrum, RGBAlbedoSpectrum,RGBUnboundedSpectrum, RGBIlluminantSpectrum,BlackBodySpectrum>
{
public:using TaggedPointer::TaggedPointer;std::string ToString() const;float operator()(float lambda) const;float MaxValue() const;
};using:此处不是「引入命名空间」,而是「将基类的构造函数引入到当前类的作用域中」,让当前类(Spectrum)的对象可以直接使用基类的所有构造函数来初始化。
如何知道是哪个类型
首先using Types = TypePack<Ts...>;这个就是相当于一个类型的pack,通过下面的方式得到是模版中的第几个
template<typename... Ts>
struct TypePack
{static constexpr size_t count = sizeof...(Ts);
};template <typename T, typename... Ts>
struct IndexOf
{static constexpr int count = 0;static_assert(!std::is_same_v<T, T>, "Type not present in TypePack");
};template<typename T, typename... Ts>
struct IndexOf<T, TypePack<T, Ts...>>
{static constexpr int count = 0;
};template <typename T, typename U, typename... Ts>
struct IndexOf<T, TypePack<U, Ts...>>
{static constexpr int count = 1 + IndexOf<T, TypePack<Ts...>>::count;
};- 首先输入一个匹配的参数T, TypePack<Ts...>然后这个Ts会根据struct IndexOf<T, TypePack<U, Ts...>>还是struct IndexOf<T, TypePack<T, Ts...>>也就是参数列表的第一个参数来决定是否为同类型。
- 最后如果匹配到后面就是T, TypePack<>这样就是匹配默认的struct IndexOf因为typename... Ts可以匹配任何参数。然后!std::is_same_v<T, T>一定为false,也就是会产生断言.
模板推导时,nullptr 会被推导为 std::nullptr_t 类型
std::remove_cv_t会去掉const 和 volatile
class TaggedPointer
{
public:
......template <typename T>static constexpr unsigned int TypeIndex(){using Tp = typename std::remove_cv_t<T>;if constexpr (std::is_same_v<Tp, std::nullptr_t>)return 0;else return 1 + rtw::IndexOf<Tp, Types>::count;}
......
};通过上面的方式我们就可以直接的拿到对应的TaggedPointer的对应的类型码。
我们要如何根据不同的type去调用对应的函数?
这里就需要我们定义一个Dispatch函数。这个函数什么作用呢?因为我们的高地址是类型,低地址为实际的对象的地址。也就是我们需要。根据具体的类型比如T,然后转为对应的(T*)(ptr & ptrmask)相当于把低位的对象地址拿出来,然后调用对应的成员函数。这个调用的函数是编译的时候确定的。
我们这里实现operator()的操作,就是根据lambda波长得到对应的光谱的值。
但 Lambda 表达式的 auto 参数是特殊情况——C++11 引入「泛型 Lambda」,允许 Lambda 的参数用 auto,本质是编译器自动生成「模板化的闭包类」,auto 会被推导为模板参数,并非普通的 “auto 传参”。
auto推导:仅推导基础类型,但会「丢弃引用、cv 限定符(const/volatile)和左 / 右值属性」。例如:
auto x = 3;→x是int(正确);int& y = x; auto z = y;→z是int(丢弃了&,变成值类型);const int a = 5; auto b = a;→b是int(丢弃了const)。
decltype(表达式)推导:不仅推导类型,还会「精准保留表达式的引用属性、cv 限定符和左 / 右值属性」。例如:
int& y = x; decltype(y) z = y;→z是int&(保留引用);const int a = 5; decltype(a) b = a;→b是const int(保留 const);decltype(3 + 4) c;→c是int(右值表达式,推导为值类型)。
而 decltype(auto) 做的是:用 auto 的语法简化类型书写,同时用 decltype 的规则推导类型和保留属性—— 本质是「decltype(表达式) 的语法糖」,但无需显式写表达式(表达式就是 auto 对应的初始化值 / 返回值)。
实现Dispatch
- 这个Dispatch首先我们得知道,就是它应该是多种类型的比如这里是Spectrum中的某些函数。或者是xxx的某些函数,或者是Spectrum的其他函数,比如tostring这些。这些函数他们的返回类型是不确定的。
- 比如我们下面需要它返回float
float Spectrum::operator()(float lambda) const
{auto op = [&](auto ptr) { return (*ptr)(lambda); };return Dispatch(op);
}- 所以我们需要定义一个它的泛型返回值。也就是我们需要自动的推断它的类型。decltype可以推断括号内部表达式的值,而这里就可以使用decltype(auto)去推断返回值。
- 这里我们需要知道因为我们的返回类型是不确定的。也就是在很多地方都会调用这个Dispatch函数。但是C++ 是 静态类型语言,所有函数的返回类型都必须在编译期确定—— 不存在 “运行时才动态确定返回类型” 的函数也就是我们得在每次调用的时候确定Dispatch会返回什么?
- 首先我们必须明确这个Dispatch的作用在于我们的工厂去调用我们产品的某些方法,然后这个Dispatch根据调用的指针的和类型判断编译期间确定调用哪个。
- 也就是在调用的时候,我们的泛型参数列表Ts...是确定的。我们的调用函数是确定的。那么我们就可以根据泛型列表对应的数据类型中的对应的成员函数去在编译期间确定不同的调用位置的返回类型。
- 我们需要有一个方式来得到根据上面的条件,也就是Ts还有调用的函数来得到返回值的方法。
- 文中的逻辑是使用c++的语法糖std::invoke_result_t比如下面。他可以根据参数列表和函数去返回这个这个函数返回的类型。
#include <type_traits>
#include <iostream>int add(int a, int b) {return a + b;
}int main() {// 使用 invoke_result_t 获取函数返回类型using ResultType = std::invoke_result_t<decltype(add), int, int>;static_assert(std::is_same_v<ResultType, int>, "Return type should be int");std::cout << "add function returns: " << typeid(ResultType).name() << std::endl;return 0;
}struct Multiplier {double operator()(double a, double b) const {return a * b;}
};int main() {using ResultType = std::invoke_result_t<Multiplier, double, double>;static_assert(std::is_same_v<ResultType, double>,"Return type should be double");return 0;
}template<class F, class... ArgTypes>
struct invoke_result;template<class F, class... ArgTypes>
using invoke_result_t = typename invoke_result<F, ArgTypes...>::type;
- 这里如果是std::invoke_result<F, Ts*>... 就会根据每个类型使用*的指针类型然后得到每个类型针对这个F的返回值。pbrt根据每个返回值判断是不是同一个返回值。还是使用的模版的方式和std::is_same_v去判断是不是同一个类型
- 这个...相当于解包,也就是将多个类型展开,实际上参数列表还是U, T1, T2, T3...
template<typename... Ts>struct IsSameType;template<>struct IsSameType<>{static constexpr bool value = true;};template<typename T>struct IsSameType<T>{static constexpr bool value = true;};template<typename T, typename U, typename... Ts>struct IsSameType<T, U, Ts...>{static constexpr bool value = (std::is_same_v<T, U> && IsSameType<U, Ts...>::value);};- 通过下面的方式我们就可以得到对应的返回类型
template<typename... Ts>
struct SameType;template<typename T, typename... Ts>
struct SameType<T, Ts...>
{
using type = T;
static_assert(IsSameType<T, Ts...>::value, "Not all types in pack are the same");
};template<typename F, typename... Ts>
struct ReturnType
{
using type = typename SameType<std::invoke_result_t<F, Ts*>...>::type;
};
template<typename F>decltype(auto) Dispatch(F&& func) const{using R = typename detail::ReturnType<F, Ts...>::type;}- 接下来我们需要根据对应的对象指针的地址调用对应的函数。也就是dispatch的后半段。
- 这里我们需要去调用F有一个问题,就是我们是根据你的类型去决定那个模版去调用对应的函数,比如T1去调用。那么就会有一个问题,到底有多少个参数呢?因为之前是类似类型的判断可以递归的调用,这次是需要知道是第几个type然后转换为对应的指针类型去调用,因为成员函数是根据指针类型来调用的。这里有一个方式就是你有几个模版的类型,比如这里是7个模版,就写一个7个模版的对应的转换函数。也就是重载。
template<typename R, typename F, typename T>
R Dispatch(F&& func, const void* ptr, int index)
{return func((const T*)ptr);
}template<typename R, typename F, typename T>
R Dispatch(F&& func, void* ptr, int index)
{return func((T*)ptr);
}template<typename R, typename F, typename T0, typename T1, typename T2,
typename T3, typename T4, typename T5, typename T6>
R Dispatch(F&& func, const void* ptr, int index)
{switch (index){case 0:return func((const T0*)ptr);case 1: return func((const T1*)ptr);case 2:return func((const T2*)ptr);case 3:return func((const T3*)ptr);case 4:return func((const T4*)ptr);case 5:return func((const T5*)ptr);case 6:return func((const T6*)ptr);}
}template<typename R, typename F, typename T0, typename T1, typename T2,
typename T3, typename T4, typename T5, typename T6>
R Dispatch(F&& func, void* ptr, int index)
{switch (index){case 0:return func((const T0*)ptr);case 1: return func((const T1*)ptr);case 2:return func((const T2*)ptr);case 3:return func((const T3*)ptr);case 4:return func((const T4*)ptr);case 5:return func((const T5*)ptr);case 6:return func((const T6*)ptr);}
}最后我们实现了这个Dispatch。
void* ptr() { return reinterpret_cast<void*>(bits & ptrMask); }const void* ptr() const { return reinterpret_cast<const void*>(bits * ptrMask); }unsigned int Tag() const { return ((bits & tagMask) >> tagShift); }template<typename F>
decltype(auto) Dispatch(F&& func) const
{using R = typename detail::ReturnType<F, Ts...>::type;return detail::Dispatch<R, F, Ts...>(func, ptr(), Tag() - 1);
}写一个demo巩固一下这种用法
#include <iostream>
template<typename... Ts>
struct TypePack
{static constexpr size_t count = sizeof...(Ts);
};template<typename T, typename... Ts>
struct IndexOf
{static constexpr int count = 0;static_assert(!std::is_same_v<T, T>, "Type not present in TypePack");
};template<typename T, typename... Ts>
struct IndexOf<T, TypePack<T, Ts...>>
{static constexpr int count = 0;
};template<typename T, typename U, typename... Ts>
struct IndexOf<T, TypePack<U, Ts...>>
{static constexpr int count = 1 + IndexOf<T, TypePack<Ts...>>::count;
};template<typename... Ts>
struct IsSameType;template<>
struct IsSameType<>
{static constexpr bool value = true;
};template<typename T>
struct IsSameType<T>
{static constexpr bool value = true;
};template<typename T, typename U, typename... Ts>
struct IsSameType<T, U, Ts...>
{static constexpr bool value = std::is_same_v<T, U> && IsSameType<U, Ts...>::value;
};template<typename... Ts>
struct SameType;template<typename T, typename... Ts>
struct SameType<T, Ts...>
{using type = T;static_assert(IsSameType<T, Ts...>::value, "Types must be the same");
};template<typename F, typename... Ts>
struct ReturnType
{using type = SameType<std::invoke_result_t<F, Ts*>...>::type;
};namespace detail
{template<typename R, typename F, typename T0, typename T1, typename T2>R Dispatch(F&& func, void* ptr, int index){switch(index){case 0: return func((T0*)ptr);case 1: return func((T1*)ptr);default: return func((T2*)ptr);};}}template<typename... Ts>
class TaggedPointer
{
public:using Types = TypePack<Ts...>;template<typename T>static constexpr unsigned int TypeIndex(){using Tp = typename std::remove_cv_t<T>;if constexpr(std::is_same_v<Tp, std::nullptr_t>)return 0;else{return 1 + IndexOf<Tp, Types>::count;}}template<typename T>TaggedPointer(T* ptr){uint64_t iptr = reinterpret_cast<uint64_t>(ptr);constexpr unsigned int type = TypeIndex<T>();bits = iptr | ((uint64_t)type << tagShift);}void* ptr() const {return reinterpret_cast<void*>( bits & ptrMask );}unsigned int tag() const{return ((bits & tagMask) >> tagShift);}template<typename F>decltype(auto) Dispatch(F&& func) const{using R = ReturnType<F, Ts...>::type;return detail::Dispatch<R, F, Ts...>(func, ptr(), tag() - 1);}private:static constexpr int tagShift = 57;static constexpr int tagBits = 64 - tagShift;static constexpr uint64_t tagMask = ((1ull << tagBits) - 1) << tagShift;static constexpr uint64_t ptrMask = ~tagMask;uint64_t bits = 0;
};
#include <iostream>
#include "taggedpointer.h"
class Test1
{
public:int testFunc(){std::cout << "Test1::testFunc()" << std::endl;return 1;}
};class Test2
{
public:int testFunc(){std::cout << "Test2::testFunc()" << std::endl;return 2;}
};class Test3
{
public:int testFunc(){std::cout << "Test3::testFunc()" << std::endl;return 3;}
};class Test : public TaggedPointer<Test1, Test2, Test3>
{
public:using TaggedPointer::TaggedPointer;int testFunc(){auto op = [](auto ptr) { return ptr->testFunc(); };return Dispatch(op);}
};
#include <iostream>
#include "temp.h"
int main()
{Test1 test1;Test2 test2;Test3 test3;Test t1(&test1);int r1 = t1.testFunc();std::cout << "r1 = " << r1 << std::endl;Test t2(&test2);int r2 = t2.testFunc();std::cout << "r2 = " << r2 << std::endl;Test t3(&test3);int r3 = t3.testFunc();std::cout << "r3 = " << r3 << std::endl;return 0;
}
