c++templatesmetaprogrammingtemplate-meta-programmingexplicit-instantiation

Automating explicit template instantiation


To reduce compile times in a template-heavy project, I'm trying to explicitly instantiate many templates in a separate compilation unit. Because these templates depend on enum class members, I'm able to list all possible instantiations. I want all other cpp-files to only see the declaration. While I'm able to do this, I run into problems trying to factorize the explicit instantiations. I will first explain 2 working examples below, in order to explain what exactly my issue is (example 3):

Example 1

/* test.h
   Contains the function-template-declaration, not the implementation.
*/

enum class Enum
{
    Member1,
    Member2,
    Member3
};

struct Type
{
    template <Enum Value>
    int get() const;
};
/* test.cpp
   Only the declaration is visible -> needs to link against correct instantiation.
*/

#include "test.h"

int main() {
    std::cout << Type{}.get<Enum::Member1>() << '\n';
}
/* test.tpp 
   .tpp extension indicates that it contains template implementations.
*/

#include "test.h"

template <Enum Value>
int Type::get() const
{
    return static_cast<int>(Value); // silly implementation
}
/* instantiate.cpp
   Explicitly instantiate for each of the enum members.
*/

#include "test.tpp"

template int Type::get<Enum::Member1>() const;
template int Type::get<Enum::Member2>() const;
template int Type::get<Enum::Member3>() const;

As mentioned before, the above compiles and links without issues. However, in the real application, I have many function-templates and many more enum-members. Therefore, I tried making my life somewhat easier by grouping the members together in a new class, which itself depends on the template parameter and explicitly instantiate this class for each of the enum-values.

Example 2

// instantiate.cpp

#include "test.tpp"

template <Enum Value>
struct Instantiate
{
    using Function = int (Type::*)() const;
    static constexpr Function f1 = Type::get<Value>;
   
    // many more member-functions
};

template class Instantiate<Enum::Member1>;
template class Instantiate<Enum::Member2>;
template class Instantiate<Enum::Member3>;

This still works (because in order to initialize a pointer to a member, this member has to be instantiated), but when the number of enum-members is large, it will still be messy. Now I can finally get to the issue. I thought I could factorize even further by defining a new class-template that depends on a parameter pack, which then derives from each of the types in the pack like so:

Example 3

// instantiate.cpp

#include "test.tpp"

template <Enum Value>
struct Instantiate { /* same as before */ };

template <Enum ... Pack>
struct InstantiateAll:
    Instantiate<Pack> ...
{};

template class InstantiateAll<Enum::Member1, Enum::Member2, Enum::Member3>; 

This should work, right? In order to instantiate InstantiateAll<...>, each of the derived classes have to be instantiated. At least, this is what I thought. The above compiles but results in a linker-error. Upon checking the symbol-table of instantiate.o with nm, it's confirmed that nothing at all has been instantiated. Why not?

Of course, I can get by using example 2, but it really got me curious why things break down like this.

(Compiling with GCC 10.2.0)

Edit: same happens on Clang 8.0.1 (although I have to use the address-of-operator explicitly in assigning the function-pointers: Function f1 = &Type::get<Value>;)

Edit: User 2b-t kindly made the examples available through https://www.onlinegdb.com/HyGr7w0fv_ for people to experiment with.


Solution

  • If the compiler sees that code isn't referred to, even for static initialization with side effects, it can eliminate it, and I think that's the case in your example. It can "prove" those class instantiations are not used, and so the side effects are lost.

    For a non-standard solution, but one that works on g++ (and presumably clang, but not tested) is to mark your static data members with the "used" attribute:

    template <Enum Value>
    struct Instantiate
    {
        using Function = int (Type::*)() const;
        static constexpr Function f1 __attribute__((used)) = &Type::get<Value>;
       
        // many more member-functions
    };
    

    Update

    Reviewing the standard, the wording seems like I got it exactly backwards:

    "If an object of static storage duration has initialization or a destructor with side effects, it shall not be eliminated even if it appears to be unused, except that a class object or its copy may be eliminated as specified in ..."

    So I've had this in my head for decades, and now I'm uncertain as to what I was thinking. :) But it seems related, given the attribute helps. But now I have to learn what's going on.