将 C++ 模板函数定义存储在 .CPP 文件

Storing C++ template function definitions in a .CPP file

提问人:Rob 提问时间:9/22/2008 最后编辑:Leon TimmermansRob 更新时间:9/21/2022 访问量:554014

问:

我有一些模板代码,我希望将其存储在 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::d o 模板函数仅与 ints 和 std::strings 一起使用,因此这些定义意味着应用程序将链接。

我的问题是 - 这是一个令人讨厌的黑客攻击,还是可以与其他编译器/链接器一起使用?我目前只在 VS2008 中使用此代码,但希望移植到其他环境。

C++ 模板

评论

111赞 Quentin 1/14/2015
让我感到震惊的是用作标识符:pdo
0赞 Ciro Santilli OurBigBook.com 1/6/2020
相关新闻: stackoverflow.com/questions/495021/...

答:

4赞 Lou Franco 9/23/2008 #1

是的,这是执行 specializiation 显式实例化的标准方法。如您所说,您不能使用其他类型实例化此模板。

编辑:根据评论更正。

评论

0赞 Richard Corden 9/23/2008
对术语很挑剔,这是一个“显式实例化”。
316赞 Aaron N. Tubbs 9/23/2008 #2

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

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

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

评论

51赞 ivotron 5/2/2011
为了补充答案,引用的链接肯定地回答了这个问题,即可以按照 Rob 的建议去做,并让代码可移植。
288赞 Ident 8/17/2015
你能在答案本身中发布相关部分吗?为什么 SO 甚至允许这样的引用。我不知道在此链接中寻找什么,因为它已经发生了重大变化。
17赞 moonshadow 9/23/2008 #3

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

4赞 Ben Collins 9/23/2008 #4

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

有关此内容,请参阅FAQ-lite

评论

3赞 paercebal 9/23/2008
AFAIK,出口已经死了,因为他们面临着越来越新的问题,每次他们解决最后一个问题,使整体解决方案变得越来越复杂。而且“export”关键字无论如何都无法让您从 CPP 中“导出”(无论如何仍然来自 H. Sutter's)。所以我说:不要屏住呼吸......
2赞 Zan Lynx 3/13/2012
若要实现导出,编译器仍需要完整的模板定义。你所得到的只是以一种编译的形式拥有它。但实际上没有意义。
2赞 DevSolar 8/26/2015
...而且它已经从标准中消失了,因为过度复杂化以获得最小的收益。
27赞 Konrad Rudolph 9/23/2008 #5

此代码格式正确。您只需要注意模板的定义在实例化时是可见的。引用该标准,§ 14.7.2.4:

类模板的非导出函数模板、非导出成员函数模板或类模板的非导出成员函数或静态数据成员的定义应存在于显式实例化的每个转换单元中。

评论

3赞 Dan Nissenbaum 6/13/2014
non-exported 是什么意思?
2赞 Konrad Rudolph 6/13/2014
@Dan 仅在其编译单元内部可见,而不能在其外部可见。如果将多个编译单元链接在一起,则导出的符号可以在它们之间使用(并且必须具有单个,或者至少在模板的情况下具有一致的定义,否则会遇到 UB)。
0赞 Dan Nissenbaum 6/13/2014
谢谢。我认为所有函数(默认情况下)在编译单元之外都是可见的。如果我有两个编译单元(定义函数)和(定义函数),那么这将成功链接。如果我是对的,那么上面的引述似乎不适用于典型情况......我是不是哪里出错了?a.cppa() {}b.cppb() { a() }
0赞 Konrad Rudolph 6/13/2014
@Dan 琐碎的反例:函数inline
1赞 Konrad Rudolph 6/13/2014
@Dan 函数模板是隐式的。原因是如果没有标准化的C++ ABI,就很难/不可能定义否则会产生的效果。inline
0赞 Benoît 10/27/2008 #6

你给出的例子没有错。但我必须说,我相信将函数定义存储在 cpp 文件中是无效的。我只理解需要将函数的声明和定义分开。

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

评论

10赞 Cody Gray - on strike 6/25/2013
它有什么低效之处?
5赞 Red XIII 11/7/2011 #7

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

评论

0赞 Dan Nissenbaum 6/13/2014
你确定它会打破ODR吗?如果 TemplateClassInst.cpp 中的实例化行引用相同的源文件(包含模板函数定义),这难道不能保证不会违反 ODR 吗,因为所有定义都是相同的(即使重复)?
0赞 nonremovable 6/22/2018
请问,什么是ODR?
0赞 andreee 9/16/2022
@nonremovable:ODR代表“一个定义规则”。有关说明,请参阅此处
175赞 namespace sid 12/19/2012 #8

对于此页面上的其他人想知道显式模板专用化(或至少在 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>;

评论

22赞 0x26res 2/21/2013
您的意思是“用于显式 CLASS 模板专业化”吗?在这种情况下,这将涵盖模板化类具有的所有功能吗?
0赞 user1633272 10/3/2019
@Arthur似乎不是,我有一些模板方法留在标题中,而 cpp 中的大多数其他方法都可以正常工作。非常好的解决方案。
0赞 user253751 3/6/2020
在提问者的情况下,他们有一个函数模板,而不是类模板。
1赞 Petko Kamenov 3/9/2021
所以,你可以把多个模板类 foo<...>某个文件的底部,对吧?所以,一个文件用于 int 的定义,例如,其他用于 float,如果有任何差异,如果没有差异,您可以在 int 下 pul 模板类 foo<float>?我做对了吗?
4赞 RichieHH 9/7/2021
我对你在这里使用 typename AND 类完全感到困惑。.
1赞 Didii 3/22/2013 #9

是时候更新了!创建一个内联(.inl,或任何其他)文件,只需将所有定义复制到其中即可。请务必在每个函数 () 上方添加模板。现在,您没有将头文件包含在内联文件中,而是反其道而行之。在类声明 () 包含内联文件。template <typename T, ...>#include "file.inl"

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

评论

29赞 Cody Gray - on strike 6/25/2013
直接的缺点是它与直接在标头中定义模板函数基本相同。一旦你 ,预处理器将直接将 的内容粘贴到标头中。无论您出于何种原因想要避免在标头中实现,此解决方案都无法解决该问题。#include "file.inl"file.inl
5赞 underscore_d 8/3/2016
- 这意味着,从技术上讲,你不必要地承担了编写所有冗长、令人费解的行外定义所需的样板的任务。我明白为什么人们想要这样做 - 实现与非模板声明/定义的最大对等,保持接口声明看起来整洁,等等 - 但这并不总是值得麻烦的。这是一个评估双方权衡并选择最不坏的案例。...直到命名空间类成为事物:O[请成为一件事template]
2赞 underscore_d 9/14/2016
@Andrew 它似乎被卡在了委员会的管道中,尽管我想我看到有人说这不是故意的。我希望它能进入C++17。也许下一个十年。
0赞 kiloalphaindia 5/7/2018
@CodyGray:从技术上讲,这对编译器来说确实是一样的,因此它不会减少编译时间。尽管如此,我认为这还是值得一提的,并在我见过的许多项目中进行了实践。沿着这条路走下去有助于将接口与定义分开,这是一个很好的做法。在这种情况下,它对 ABI 兼容性等没有帮助,但它简化了对界面的阅读和理解。
34赞 Cameron Tacklind 12/23/2016 #10

您的示例是正确的,但不是很便携。 还可以使用稍微干净的语法(正如 @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`
}

傅。MyType.cpp - 使用库,显式模板实例化 foo<MyType>

// 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>;

根据需要组织实施:

  • 所有实现都集中在一个文件中
  • 多个实现文件,每种类型一个
  • 每组类型的实现文件

为什么??

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

使用示例

傅。MyType.hpp - 需要了解 foo<MyType> 的公共接口,但不需要了解.cpp

#pragma once

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

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

例子.cpp - 可以引用本地声明,但也不会重新编译 foo<MyType>

#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);
}

错误 .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();
}

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


替代方法来自:https://stackoverflow.com/a/495056/4612476

评论

3赞 Cameron Tacklind 3/30/2017
将实际编译版本的实现细节(又称定义)和声明(in )分开。我不喜欢大多数 C++ 模板完全在头文件中定义。这与每个类/命名空间/任何分组的 C/C++ 标准背道而驰。人们似乎仍然使用整体式头文件,仅仅是因为这种替代方案没有被广泛使用或广为人知。foo.cppfoo-impl.cppfoo.hc[pp]/h
1赞 WaterGenie 12/28/2018
@MK。我首先将显式模板实例化放在源文件中定义的末尾,直到我需要在其他地方进行进一步的实例化(例如,使用模拟作为模板类型的单元测试)。这种分离允许我在外部添加更多实例化。此外,当我将原始实例作为一对保留时,它仍然有效,尽管我不得不将原始实例化列表包围在包含保护中,但我仍然可以像往常一样编译。不过,我对C++仍然很陌生,很想知道这种混合用法是否有任何额外的警告。h/cppfoo.cpp
4赞 Shmuel Levine 5/29/2019
我认为最好是解耦和 .不要在文件中;相反,请添加声明 to 以防止编译器在编译时实例化模板。确保生成系统生成这两个文件,并将这两个目标文件传递给链接器。这有多种好处:a)很明显,没有实例化;b) 对 foo.cpp 的更改不需要重新编译 foo-impl.cpp。foo.cppfoo-impl.cpp#include "foo.cpp"foo-impl.cppextern template class foo<int>;foo.cppfoo.cpp.cppfoo.cpp
10赞 Wormer 10/31/2019
这是解决模板定义问题的一种非常好的方法,它采用了两全其美的方法 - 常用类型的标头实现和实例化。我要对此设置进行的唯一更改是重命名为 和 just .我还会为 from 的实例化添加 typedefs ,同样。诀窍是为用户提供两个标头接口供用户选择。当用户需要预定义的实例化时,他会包含,当用户需要一些无序的东西时,他会包含。foo.cppfoo_impl.hfoo-impl.cppfoo.cppfoo.cppfoo.husing foo_int = foo<int>;foo.hfoo_impl.h
2赞 idbrii 8/11/2021
不应该像 cmake 这样的项目生成工具知道它不应该直接编译吗?lib/foo.cpplib/foo.inl
9赞 user9599745 8/22/2018 #11

这是定义模板函数的标准方法。我认为我读到三种定义模板的方法。或者可能是 4 个。每个都有优点和缺点。

  1. 在类定义中定义。我一点也不喜欢这样,因为我认为类定义仅供参考,应该易于阅读。但是,在课堂上定义模板比在课堂外定义模板要麻烦得多。并非所有模板声明都处于相同的复杂程度。此方法还使模板成为真正的模板。

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

  3. 包括 header.h 和实现。CPP 进入您的主线。CPP。我认为这就是它的方式。您无需准备任何预实例化,它的行为就像一个真正的模板。我的问题是它不自然。我们通常不包括并期望包含源文件。我想既然您包含了源文件,那么模板函数就可以内联了。

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

请注意,我不知道臃肿的 obj 文件的后果。

7赞 TarmoPikaro 6/28/2019 #12

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

//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 函数,建议使用前面的示例。

评论

0赞 12/27/2020
优秀的答案
2赞 KronuZ 5/14/2020 #13

以上都不适合我,所以这是你如何解决的,我的班级只有 1 个模板化的方法。

.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

评论

1赞 Mahmut EFE 1/24/2021
你的答案和问题一样,它不起作用!
1赞 Code Doggo 5/16/2022
投了反对票,但是当您实际尝试将头文件包含在另一个源文件中以开始使用此模板化类时,此代码无法编译和工作。您将收到未解决的符号链接器错误。