【问题标题】:Storing C++ template function definitions in a .CPP file将 C++ 模板函数定义存储在 .CPP 文件中
【发布时间】:2023-04-01 13:29:01
【问题描述】:

我有一些模板代码,我希望将其存储在 CPP 文件中,而不是内联在标题中。我知道只要您知道将使用哪些模板类型,就可以做到这一点。例如:

.h 文件

class foo
{
public:
    template <typename T>
    void do(const T& t);
};

.cpp 文件

template <typename T>
void foo::do(const T& t)
{
    // Do something with t
}

template void foo::do<int>(const int&);
template void foo::do<std::string>(const std::string&);

注意最后两行 - foo::do 模板函数仅用于 ints 和 std::strings,因此这些定义意味着应用程序将链接。

我的问题是 - 这是一个讨厌的 hack 还是可以与其他编译器/链接器一起使用?我目前仅将此代码与 VS2008 一起使用,但希望移植到其他环境。

【问题讨论】:

  • 我不知道这是可能的 - 一个有趣的技巧!知道这一点会有助于最近的一些任务 - 干杯!
  • 让我印象深刻的是do作为标识符的使用:p
  • 我用 gcc 做了一些类似的事情,但仍在研究中
  • 这不是“hack”,而是前向减速。这在语言标准中占有一席之地;所以是的,每个标准的编译器都允许它。
  • 如果你有几十个方法怎么办?你能在 .cpp 文件的末尾加上template class foo&lt;int&gt;;template class foo&lt;std::string&gt;; 吗?

标签: c++ templates


【解决方案1】:

您描述的问题可以通过在标题中定义模板来解决,或者通过您上面描述的方法。

我建议阅读C++ FAQ Lite中的以下几点:

他们详细介绍了这些(和其他)模板问题。

【讨论】:

  • 只是为了补充答案,引用的链接肯定地回答了问题,即可以按照 Rob 的建议进行操作,并使代码具有可移植性。
  • 您可以在答案本身中发布相关部分吗?为什么在 SO 上甚至允许这样的引用。我不知道在这个链接中寻找什么,因为它已经发生了很大的变化。
【解决方案2】:

对于此页面上的其他人想知道显式模板专业化(或至少在 VS2008 中)的正确语法是什么(就像我一样),它如下...

在您的 .h 文件中...

template<typename T>
class foo
{
public:
    void bar(const T &t);
};

在你的 .cpp 文件中

template <class T>
void foo<T>::bar(const T &t)
{ }

// Explicit template instantiation
template class foo<int>;

【讨论】:

  • 您的意思是“用于显式 CLASS 模板专业化”。在那种情况下,这将涵盖模板类具有的所有功能吗?
  • @Arthur 似乎没有,我有一些模板方法保留在标题中,而 cpp 中的大多数其他方法都可以正常工作。非常好的解决方案。
  • 在提问者的情况下,他们有一个函数模板,而不是一个类模板。
  • 那么,你可以把多个模板类 foo<...> 放在某个文件的底部,对吧?因此,一个文件用于定义 int,例如,Other for float,如果有任何差异,如果没有差异,您可以在 int 下 pul 模板类 foo?我做对了吗?
  • 我对你在这里使用 typename AND class 完全感到困惑..
【解决方案3】:

这段代码格式正确。您只需要注意模板的定义在实例化点是可见的。引用标准,第 14.7.2.4 节:

类模板的非导出函数模板、非导出成员函数模板或非导出成员函数或静态数据成员的定义应出现在显式实例化它的每个翻译单元中。

【讨论】:

  • 非出口是什么意思?
  • @Dan 仅在其编译单元内部可见,而不在其外部可见。如果将多个编译单元链接在一起,则可以在它们之间使用导出的符号(并且必须有一个,或者至少,在模板的情况下,必须有一个一致的定义,否则你会遇到 UB)。
  • 谢谢。我认为所有功能(默认情况下)在编译单元之外都是可见的。如果我有两个编译单元a.cpp(定义函数a() {})和b.cpp(定义函数b() { a() }),那么这将成功链接。如果我是对的,那么上面的引用似乎不适用于典型案例......我是不是哪里出错了?
  • @Dan Trivial 反例:inline 函数
  • @Dan 函数模板隐含为inline。原因是如果没有标准化的 C++ ABI,很难/不可能定义否则会产生的影响。
【解决方案4】:

您的示例是正确的,但不是很便携。 还有一种更简洁的语法可以使用(正如@namespace-sid 等指出的那样)。

但是,假设模板类是某个要共享的库的一部分...

是否应该编译其他版本的模板类?

库维护者是否应该预测类的所有可能的模板化使用?

另一种方法

在您的源代码中添加第三个文件,即模板实现/实例化文件。

lib/foo.hpp 在/来自图书馆

#pragma once

template <typename T>
class foo
{
public:
    void bar(const T&);
};

lib/foo.cpp 直接编译这个文件只会浪费编译时间

// Include guard here, just in case
#pragma once

#include "foo.hpp"

template <typename T>
void foo::bar(const T& arg)
{
    // Do something with `arg`
}

foo.MyType.cpp 使用库,foo&lt;MyType&gt;的显式模板实例化

// Consider adding "anti-guard" to make sure it's not included in other translation units
#if __INCLUDE_LEVEL__
  #error "Don't include this file"
#endif

// Yes, we include the .cpp file
#include <lib/foo.cpp>
#include "MyType.hpp"

template class foo<MyType>;

当然,您可以在第三个文件中有多个实现。 或者,您可能需要多个实现文件,例如,针对您想要使用的每种类型(或类型集)一个。

此设置应减少编译时间,尤其是对于大量使用的复杂模板代码,因为您不会在每个代码中重新编译相同的头文件 翻译单元。 它还可以通过编译器和构建脚本更好地检测哪些代码需要重新编译,从而减少增量构建负担。

使用示例

foo.MyType.hpp 需要了解foo&lt;MyType&gt; 的公共接口,而不是.cpp 来源

#pragma once

#include <lib/foo.hpp>
#include "MyType.hpp"

// Declare `temp`. Doesn't need to include `foo.cpp`
extern foo<MyType> temp;

examples.cpp 可以引用本地声明但也不能重新编译foo&lt;MyType&gt;

#include "foo.MyType.hpp"

MyType instance;

// Define `temp`. Doesn't need to include `foo.cpp`
foo<MyType> temp;

void example_1() {
    // Use `temp`
    temp.bar(instance);
}

void example_2() {
    // Function local instance
    foo<MyType> temp2;

    // Use templated library function
    temp2.bar(instance);
}

error.cpp 适用于纯标题模板但此处不适用的示例

#include <lib/foo.hpp>

// Causes compilation errors at link time since we never had the explicit instantiation:
// template class foo<int>;
// GCC linker gives an error: "undefined reference to `foo<int>::bar()'"
foo<int> nonExplicitlyInstantiatedTemplate;
void linkerError()
{
    nonExplicitlyInstantiatedTemplate.bar();
}

请注意,大多数编译器/linter/代码助手不会将此检测为错误,因为根据 C++ 标准没有错误。 但是,当您将此翻译单元链接到完整的可执行文件时,链接器将找不到foo&lt;int&gt; 的定义版本。


如果没记错的话,我最初是从 SO 那里得到这个想法的。但是当我写下这个答案时,我一辈子都找不到原来的 SOA。今天,我想我找到了:https://stackoverflow.com/a/495056/4612476

【讨论】:

  • 分离实现细节(又名foo.cpp 中的定义),从中实际编译版本(foo-impl.cpp)和声明(foo.h)。我不喜欢大多数 C++ 模板完全在头文件中定义。这与您使用的每个类/命名空间/任何分组的 c[pp]/h 对的 C/C++ 标准相反。人们似乎仍在使用单一的头文件,仅仅是因为这种替代方案没有被广泛使用或为人所知。
  • @MK。起初,我将显式模板实例化放在源文件定义的末尾,直到我需要在其他地方进一步实例化(例如,使用模拟作为模板类型的单元测试)。这种分离允许我在外部添加更多实例。此外,当我将原始实例保留为 h/cpp 对时,它仍然有效,尽管我必须将原始实例列表包含在包含保护中,但我仍然可以正常编译 foo.cpp。不过,我对 C++ 还是很陌生,很想知道这种混合使用是否有任何额外的警告。
  • 我认为最好将foo.cppfoo-impl.cpp解耦。不要在foo-impl.cpp文件中#include "foo.cpp";相反,将声明extern template class foo&lt;int&gt;; 添加到foo.cpp 以防止编译器在编译foo.cpp 时实例化模板。确保构建系统构建两个.cpp 文件并将两个目标文件传递给链接器。这有很多好处:a) 在foo.cpp 中很清楚没有实例化; b) 对 foo.cpp 的更改不需要重新编译 foo-impl.cpp。
  • 这是解决模板定义问题的一种非常好的方法,它可以兼顾两全其美——头文件实现和常用类型的实例化。我要对此设置进行的唯一更改是将foo.cpp 重命名为foo_impl.h 并将foo-impl.cpp 重命名为foo.cpp。我还将为从foo.cppfoo.h 的实例添加typedef,同样using foo_int = foo&lt;int&gt;;。诀窍是为用户提供两个标题接口供选择。当用户需要预定义的实例化时,他包括foo.h,当用户需要一些不正常的东西时,他包括foo_impl.h
  • 不应该 lib/foo.cpplib/foo.inl 这样像 cmake 这样的项目生成工具知道它不应该直接编译吗?
【解决方案5】:

这应该可以在任何支持模板的地方正常工作。显式模板实例化是 C++ 标准的一部分。

【讨论】:

    【解决方案6】:

    这是定义模板函数的标准方法。我认为我阅读了三种定义模板的方法。或者可能有 4 个。各有利弊。

    1. 在类定义中定义。我根本不喜欢这样,因为我认为类定义仅供参考,应该易于阅读。然而,在类中定义模板比在外面定义模板要简单得多。并不是所有的模板声明都处于相同的复杂程度。此方法还使模板成为真正的模板。

    2. 在同一标题中定义模板,但在类之外。这是我大多数时候首选的方式。它使您的类定义保持整洁,模板仍然是真正的模板。然而,它需要完整的模板命名,这可能很棘手。此外,您的代码可供所有人使用。但是,如果您需要内联代码,这是唯一的方法。您还可以通过在类定义的末尾创建一个 .INL 文件来完成此操作。

    3. 将 header.h 和 implementation.CPP 包含到您的 main.CPP 中。我认为这就是它的完成方式。您不必准备任何预实例化,它的行为就像一个真正的模板。我的问题是它不自然。我们通常不包含并期望包含源文件。我想既然你包含了源文件,模板函数可以被内联。

    4. 最后一种方法,也就是贴出的方法,是在源文件中定义模板,就像第 3 种方法一样;但是我们不包含源文件,而是将模板预实例化为我们需要的模板。我对这种方法没有任何问题,有时它会派上用场。我们有一个大代码,它不能从内联中受益,所以只需将它放在 CPP 文件中。如果我们知道常见的实例化并且我们可以预定义它们。这使我们免于将基本相同的东西写 5 次、10 次。这种方法的好处是保持我们的代码专有。但我不建议在 CPP 文件中放置微小的、经常使用的函数。因为这会降低库的性能。

    注意,我不知道 obj 文件过大的后果。

    【讨论】:

      【解决方案7】:

      这绝对不是一个讨厌的 hack,但请注意,对于要与给定模板一起使用的每个类/类型,您都必须这样做(显式模板专业化)。如果有许多类型请求模板实例化,您的 .cpp 文件中可能会有很多行。为了解决这个问题,您可以在您使用的每个项目中拥有一个 TemplateClassInst.cpp,这样您就可以更好地控制要实例化的类型。显然,这个解决方案并不完美(又名银弹),因为您最终可能会破坏 ODR :)。

      【讨论】:

      • 您确定它会破坏 ODR 吗?如果 TemplateClassInst.cpp 中的实例化行引用了相同的源文件(包含模板函数定义),由于所有定义都是相同的(即使重复),这是否保证不会违反 ODR?
      • 请问,ODR 是什么?
      【解决方案8】:

      举个例子,假设出于某种原因你想要一个模板类:

      //test_template.h:
      #pragma once
      #include <cstdio>
      
      template <class T>
      class DemoT
      {
      public:
          void test()
          {
              printf("ok\n");
          }
      };
      
      template <>
      void DemoT<int>::test()
      {
          printf("int test (int)\n");
      }
      
      
      template <>
      void DemoT<bool>::test()
      {
          printf("int test (bool)\n");
      }
      

      如果您使用 Visual Studio 编译此代码 - 它可以开箱即用。 gcc 将产生链接器错误(如果从多个 .cpp 文件中使用相同的头文件):

      error : multiple definition of `DemoT<int>::test()'; your.o: .../test_template.h:16: first defined here
      

      可以将实现移动到 .cpp 文件,但是你需要像这样声明类 -

      //test_template.h:
      #pragma once
      #include <cstdio>
      
      template <class T>
      class DemoT
      {
      public:
          void test()
          {
              printf("ok\n");
          }
      };
      
      template <>
      void DemoT<int>::test();
      
      template <>
      void DemoT<bool>::test();
      
      // Instantiate parametrized template classes, implementation resides on .cpp side.
      template class DemoT<bool>;
      template class DemoT<int>;
      

      然后 .cpp 将如下所示:

      //test_template.cpp:
      #include "test_template.h"
      
      template <>
      void DemoT<int>::test()
      {
          printf("int test (int)\n");
      }
      
      
      template <>
      void DemoT<bool>::test()
      {
          printf("int test (bool)\n");
      }
      

      头文件中没有最后两行 - gcc 可以正常工作,但 Visual Studio 会产生错误:

       error LNK2019: unresolved external symbol "public: void __cdecl DemoT<int>::test(void)" (?test@?$DemoT@H@@QEAAXXZ) referenced in function
      

      如果您想通过 .dll 导出公开函数,模板类语法是可选的,但这仅适用于 windows 平台 - 所以 test_template.h 可能如下所示:

      //test_template.h:
      #pragma once
      #include <cstdio>
      
      template <class T>
      class DemoT
      {
      public:
          void test()
          {
              printf("ok\n");
          }
      };
      
      #ifdef _WIN32
          #define DLL_EXPORT __declspec(dllexport) 
      #else
          #define DLL_EXPORT
      #endif
      
      template <>
      void DLL_EXPORT DemoT<int>::test();
      
      template <>
      void DLL_EXPORT DemoT<bool>::test();
      

      使用上一个示例中的 .cpp 文件。

      然而这让链接器更加头疼,所以如果你不导出 .dll 函数,建议使用前面的示例。

      【讨论】:

      • 优秀的答案
      【解决方案9】:

      在最新的标准中,有一个关键字 (export) 可以帮助缓解这个问题,但是除了 Comeau,我所知道的任何编译器都没有实现它。

      请参阅FAQ-lite 了解此内容。

      【讨论】:

      • AFAIK,出口已死,因为他们面临越来越新的问题,每次解决最后一个问题,使整体解决方案越来越复杂。并且“export”关键字无论如何都不会使您能够从CPP“export”(仍然是H. Sutter's)。所以我说:不要屏住呼吸......
      • 要实现导出,编译器仍然需要完整的模板定义。您所获得的只是以某种编译的形式拥有它。但真的没有任何意义。
      • ...它已偏离标准,因为过度复杂化以获得最小收益。
      【解决方案10】:

      以上都不适合我,所以这就是你解决它的方法,我的班级只有一个模板化的方法..

      .h

      class Model
      {
          template <class T>
          void build(T* b, uint32_t number);
      };
      

      .cpp

      #include "Model.h"
      template <class T>
      void Model::build(T* b, uint32_t number)
      {
          //implementation
      }
      
      void TemporaryFunction()
      {
          Model m;
          m.build<B1>(new B1(),1);
          m.build<B2>(new B2(), 1);
          m.build<B3>(new B3(), 1);
      }
      

      这避免了链接器错误,并且根本不需要调用 TemporaryFunction

      【讨论】:

      • 你的答案与问题相同,它不起作用!
      【解决方案11】:

      是的,这是进行专业化显式实例化的标准方式。正如你所说,你不能用其他类型实例化这个模板。

      编辑:根据评论更正。

      【讨论】:

      • 对术语挑剔是一种“显式实例化”。
      【解决方案12】:

      是时候更新了!创建一个内联(.inl 或任何其他)文件,然后简单地将所有定义复制到其中。请务必在每个函数上方添加模板 (template &lt;typename T, ...&gt;)。现在,您不将头文件包含在内联文件中,而是执行相反的操作。在您的类声明 (#include "file.inl") 之后包含内联文件

      我真的不知道为什么没有人提到这一点。我认为没有直接的缺点。

      【讨论】:

      • 直接的缺点是它与直接在标题中定义模板函数基本相同。一旦你#include "file.inl",预处理器会将file.inl 的内容直接粘贴到标题中。无论您出于何种原因想要避免在标头中执行实现,此解决方案都无法解决该问题。
      • - and 意味着你在技术上不必要地负担自己的任务,编写所有冗长的、令人费解的样板文件,这些样板文件是外线 @987654326 所需的@定义。我明白人们为什么要这样做——为了实现与非模板声明/定义的最大一致性,保持接口声明看起来整洁等等——但这并不总是值得麻烦的。这是一个评估双方权衡并选择最差的案例。 ...直到namespace class 成为一件事:O [请成为一件事]
      • @Andrew 它似乎卡在了委员会的管道中,尽管我想我看到有人说这不是故意的。我希望它已经成为 C++17。也许下一个十年。
      • @CodyGray:从技术上讲,这对于编译器来说确实是一样的,因此不会减少编译时间。我仍然认为这是值得一提的,并在我见过的许多项目中得到了实践。走这条路有助于将接口与定义分开,这是一个很好的做法。在这种情况下,它对 ABI 兼容性等没有帮助,但它有助于阅读和理解界面。
      【解决方案13】:

      你给出的例子没有错。但我必须说我认为将函数定义存储在 cpp 文件中效率不高。我只理解需要将函数的声明和定义分开。

      当与显式类实例化一起使用时,Boost 概念检查库 (BCCL) 可以帮助您在 cpp 文件中生成模板函数代码。

      【讨论】:

      • 它有什么低效的地方?
      猜你喜欢
      • 1970-01-01
      • 2022-07-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多