I am trying to explicitly instantiate a member function template, to keep its definition out of the header. The types that need to be instantiated for are the alternative types of an std::variant
instantiation.
I would like to avoid typing all the explicit instantiations in the source file.
Minimum exposition code:
Header:
#include <variant>
using VarType = std::variant<bool, int, float>;
class VarHolder
{
public:
template<typename T>
const T* get() const;
private:
VarType var;
};
Source:
// A trivial function, for demonstration only
template<typename T>
const T* VarHolder::get() const
{
if (std::holds_alternative<T>(var))
return &std::get<T>(var);
return nullptr;
}
// manual instantiations, which i'd like to avoid,
// since I need to keep these in sync with the definition of VarType
//template const bool* VarHolder::get<bool>() const;
//template const int* VarHolder::get<int>() const;
//template const float* VarHolder::get<float>() const;
So far I've come up with two possible solutions:
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>; // actual instantiation
This works in MSVC 2022 (C++17). I'm able to include the header and call all the instantiations of VarHolder::get()
, without getting linker errors. However I doubt it's the optimal way.
Since I'm not familiar with member function pointers, I decided to create a wrapper function template, which will call the get()
method for some type.
template<typename T>
void fn(const VarHolder& t)
{
t.get<T>();
}
using fn_t = decltype(&fn<int>); // should be void (*)(const VarHolder&)
template<typename T>
struct S;
template<typename ...Ts>
struct S<std::variant<Ts...>> {
constexpr static fn_t arr[]{ (&fn<Ts>)... }; // store pointers to all instantiations of fn()
};
template struct S<VarType>; // actual instantiation
This doesn't work in MSVC 2022. I get a linker error if calling VarHolder::get()
elsewhere.
You must explicitly instantiate the member function template.
A definition of a function template, member function of a class template, variable template, or static data member of a class template shall be reachable from the end of every definition domain ([basic.def.odr]) in which it is implicitly instantiated ([temp.inst]) unless the corresponding specialization is explicitly instantiated ([temp.explicit]) in some translation unit; no diagnostic is required.
You're trying to "cheat the system" by generating explicit instantiations implicitly, but that's not possible.
template F<VarType>;
(which should be template class F<VarType>
) attempts to explicitly instantiate F
, which then generates all those explicit instantiations for you.
However, this is not portable. If it works, it works because MSVC is unnecessarily aggressive when it comes to transitive explicit instantiations.
[temp.explicit] p10 states that only the direct non-template members will be explicitly instantiated when a class template is instantiated.
f
is obviously not a direct non-template member of F
, so this approach won't work.
Taking addresses of function templates by forming a function pointer is also not an explicit instantiation, so this would violate the first quote.
Let's look at an example:
template <typename T>
void foo() {}
template <typename T>
struct S {
S() { foo<T>(); }
};
template struct S<int>;
This explicit instantiation of S
also includes an implicit instantiation of foo<int>()
.
The generated assembly from GCC or Clang includes only: (https://godbolt.org/z/orffnM7eT)
S<int>::S() [base object constructor]:
ret
MSVC does emit foo<int>
for some reason, but I wouldn't rely on it.
If only S<int>
is emitted and foo<int>
is inlined and not found in the assembly, you get a linker error when calling foo<int>
from another object file, since it doesn't exist.
Presumably, that's why your second method gives you a linker error.
// header: define the type list and the variant type
#define VAR_HOLDER_VARIANT_TYPE_LIST(F, S) \
F(bool) S() F(int) S() F(float)
#define IDENTITY(...) __VA_ARGS__
#define COMMA() ,
using VarType = std::variant<VAR_HOLDER_VARIANT_TYPE_LIST(IDENTITY, COMMA)>;
// source: define all explicit instantiations
#define EXPLICITLY_INSTANTIATE_VAR_HOLDER_GET(...) \
template const __VA_ARGS__* VarHolder::get<__VA_ARGS__>() const
#define SEMICOLON() ;
VAR_HOLDER_VARIANT_TYPE_LIST(EXPLICITLY_INSTANTIATE_VAR_HOLDER_GET, SEMICOLON);
Macros are ugly, but at least you don't repeat yourself.
When changing the types of the variant, you only have to update VAR_HOLDER_VARIANT_TYPE_LIST
.
The rest stays the same.