C++中为std::variant备选类型自动实例化成员函数模板
我希望将成员函数模板的定义放在源文件中,因此需要显式实例化该模板,实例化目标类型为某个std::variant实例的所有备选类型。我想避免在源文件中手动编写所有显式实例化代码。
最小示例代码
头文件
#include <variant> using VarType = std::variant<bool, int, float>; class VarHolder { public: template<typename T> const T* get() const; private: VarType var; };
源文件
// 仅作演示的简单函数 template<typename T> const T* VarHolder::get() const { if (std::holds_alternative<T>(var)) return &std::get<T>(var); return nullptr; } // 我想避免的手动实例化,需与VarType定义保持同步 //template const bool* VarHolder::get<bool>() const; //template const int* VarHolder::get<int>() const; //template const float* VarHolder::get<float>() const;
我的尝试
目前我想到两种方案:
方案1:定义调用所有所需实例化的函数
template<class T> struct F; template<class ...Ts> struct F<std::variant<Ts...>> { static void f() { VarHolder t{}; ((t.get<Ts>()), ...); } }; template struct F<VarType>; // 实际实例化
该方案在MSVC 2022(C++17)中可行,包含头文件调用get()的各实例化版本不会出现链接错误,但我怀疑这不是最优方案。
方案2:存储函数指针
由于我对成员函数指针不熟悉,因此创建了一个包装函数模板来调用get()方法:
template<typename T> void fn(const VarHolder& t) { t.get<T>(); } using fn_t = decltype(&fn<int>); // 应为void (*)(const VarHolder&) template<typename T> struct S; template<typename ...Ts> struct S<std::variant<Ts...>> { constexpr static fn_t arr[]{ (&fn<Ts>)... }; // 存储所有fn()实例化的指针 }; template struct S<VarType>; // 实际实例化
该方案在MSVC 2022中无效,在其他地方调用VarHolder::get()会出现链接错误。
问题
- 第一种方案是否可移植?符合标准的编译器是否允许不进行实例化?
- 第二种方案为何失败?我原本以为获取函数指针会强制实例化函数模板。
- 是否有更优雅、可移植的方法实现我的目标?
回答
问题1:方案1的可移植性
方案1符合C++标准,具备可移植性。根据标准,显式实例化F<VarType>时,其静态成员函数f()会被实例化;而f()中对t.get<Ts>()的调用属于**odr-use(单一定义规则使用)**场景,编译器必须为VarHolder::get<Ts>()的模板实例生成代码。
极端情况下,若编译器发现f()从未被调用且无副作用,理论上可能将其优化掉,连带取消get<Ts>()的实例化,但主流编译器(GCC、Clang、MSVC)在显式实例化结构体的情况下,不会进行这种激进优化。若担心,可给f()添加[[used]]属性(C++17标准属性)强制编译器保留该函数。
问题2:方案2失败的原因
方案2的核心问题是constexpr static数组的初始化逻辑:在constexpr上下文中,如果数组从未被实际使用(比如没有代码读取数组元素),编译器会认为&fn<Ts>是未使用的常量表达式,从而跳过fn<Ts>的实例化——连带VarHolder::get<Ts>()也不会被实例化。
MSVC的行为符合标准:因为arr是constexpr static且未被使用,编译器可以优化掉它的初始化流程,自然不会触发目标模板的实例化。
问题3:更优雅、可移植的实现方法
推荐两种直接且符合标准的方案:
方案A:显式实例化包(C++17+)
借助模板特化展开std::variant的类型参数包,直接显式实例化get():
// 源文件中 template<class... Ts> struct ExplicitInstantiateGet; template<class... Ts> struct ExplicitInstantiateGet<std::variant<Ts...>> { // 展开参数包,显式实例化每个get<Ts> template const T* VarHolder::get<T>() const...; }; // 触发实例化 template struct ExplicitInstantiateGet<VarType>;
这种写法直接清晰,无冗余代码,所有主流编译器均支持。
方案B:利用std::variant::types展开(C++17+)
借助std::variant的嵌套类型types(等价于对应类型的std::tuple),简化实例化代码:
// 源文件中 template<class... Ts> void instantiate_get(std::tuple<Ts...>) { (template const T* VarHolder::get<T>() const, ...); } // 触发实例化,VarType::types是std::tuple<bool, int, float> template void instantiate_get(VarType::types{});
该写法代码更简洁,同样直接触发目标模板的显式实例化。
内容的提问来源于stack exchange,提问作者drex

