【问题标题】:Automating explicit template instantiation自动化显式模板实例化
【发布时间】:2021-07-18 22:38:55
【问题描述】:

为了减少模板繁重的项目中的编译时间,我尝试在单独的编译单元中显式实例化许多模板。因为这些模板依赖于enum class 成员,所以我能够列出所有可能的实例化。我希望所有其他 cpp 文件只看到声明。虽然我能够做到这一点,但我在尝试分解显式实例时遇到了问题。我将首先在下面解释 2 个工作示例,以解释我的问题到底是什么(示例 3):

示例 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;

如前所述,上面的编译和链接没有问题。然而,在实际应用中,我有很多函数模板和更多枚举成员。因此,我尝试通过将成员分组到一个新类中来让我的生活更轻松一些,该类本身取决于模板参数并为每个枚举值显式实例化该类。

示例 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>;

这仍然有效(因为为了初始化指向成员的指针,必须实例化该成员),但是当枚举成员的数量很大时,它仍然会很乱。现在我终于可以解决这个问题了。我想我可以通过定义一个依赖于参数包的新类模板来进一步分解,然后它从包中的每个类型派生,如下所示:

示例 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>; 

这应该可行,对吧?为了实例化InstantiateAll&lt;...&gt;,必须实例化每个派生类。至少,这是我的想法。以上编译但导致链接器错误。在用nm 检查instantiate.o 的符号表后,确认没有任何东西被实例化。为什么不呢?

当然,我可以通过使用示例 2,但它真的让我很好奇为什么事情会这样崩溃。

(使用 GCC 10.2.0 编译)

编辑:在 Clang 8.0.1 上也会发生同样的情况(尽管在分配函数指针时我必须明确使用操作符地址:Function f1 = &amp;Type::get&lt;Value&gt;;

编辑:用户 2b-t 请通过https://www.onlinegdb.com/HyGr7w0fv_ 提供示例供人们试验。

【问题讨论】:

  • 题外话:您确定需要 tpp 文件吗?我的意思是:直接在 test.h 中定义 (struct Type { template &lt;Enum Value&gt; int get () const { return static_cast&lt;int&gt;(Value); } };) 方法怎么样?
  • 我将您的代码放在一个在线编译器中:onlinegdb.com/HyGr7w0fv_ 将其添加到您的描述中可能会有所帮助,因为人们可以轻松地使用它...
  • @max66 当多个其他 cpp 文件包含头文件并使用模板时,这些函数会被编译多次。链接器会删除重复项。这会导致更长的编译和链接时间。
  • 像这样将实现移动到“.tpp”文件中不仅可以减少用户代码看到的代码量,还可以使编译器更快地运行并使用更少的内存。另一个好处是用户代码不依赖于实现或实现代码包含的头文件,因此对实现的修改不会导致重新编译用户代码 - 只会重新链接。对于常见的标题,特别是如果经常编辑,这种重新构建的节省可能是巨大的。
  • 对于技术正确性问题,C++20 [temp.explicit]/11 说:“命名类模板特化的显式实例化也是其每个成员的相同类型(声明或定义)的显式实例化 (不包括从基类继承的成员和模板成员) ...”(强调我的)。 (目前的草案已将措辞简化为“直接非模板成员”。)

标签: c++ templates metaprogramming template-meta-programming explicit-instantiation


【解决方案1】:

如果编译器发现没有引用该代码,即使对于具有副作用的静态初始化,它也可以消除它,我认为您的示例就是这种情况。它可以“证明”那些类实例化没有被使用,因此副作用就消失了。

对于非标准解决方案,但适用于 g++(可能是 clang,但未经测试)的解决方案是使用“used”属性标记您的静态数据成员:

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

更新

回顾标准,措辞似乎完全倒退了:

"如果一个静态存储持续时间的对象有初始化或 有副作用的析构函数,即使它也不会被消除 似乎没有使用,除了一个类对象或其副本可能是 按...中指定的方式消除"

所以几十年来我一直在想这个,现在我不确定我在想什么。 :) 但它似乎相关,因为属性有帮助。但现在我必须了解发生了什么。

【讨论】:

  • 谢谢!这确实有效。这对我来说仍然很奇怪,因为编译器如何“证明”这些实例没有在其他 CU 中使用?
  • 查看我对这个问题的标准评论。简而言之,基类的成员没有被实例化。
【解决方案2】:

我还不能给你一个很好的答案来解释为什么这不起作用(也许我以后可以这样做,或者其他人可以这样做),而不是让 InstantiateInstantiateAll只有一个可变参数 @ 987654324@如下作品

template <Enum ... Pack>
struct InstantiateAll {
  using Function = int (Type::*)() const;
  static constexpr std::array<Function,sizeof...(Pack)> f = {&Type::get<Pack> ...};
};
template class InstantiateAll<Enum::Member1, Enum::Member2, Enum::Member3>;

试试here

【讨论】:

  • 谢谢!这是一个可行的解决方案。我接受了 Chris 的回答,因为他的回答说明了它现在不起作用的原因。
  • 不客气。我同意他的回答给出了一个原因。我已经尝试了一些,例如,如果您让f1 调用该函数(例如,如果它是static constexpr)而不是使用函数指针,它也可以工作。我猜只是有指针,但没有人使用它使它认为它没有被使用。出乎意料但非常有趣...
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-05-02
  • 2021-11-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多