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


526

我有一些模板代码,希望将其存储在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一起使用,因此这些定义意味着该应用程序将链接。

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


22
我不知道这是可能的-一个有趣的把戏!知道这将有助于一些近期的任务-干杯!
xan

69
让我大吃一惊的是,do用作标识符:p
Quentin

我在gcc上做了类似的事情,但仍在研究
Nick

16
这不是“黑客”,而是前锋。这在语言标准中占有一席之地;因此,是的,每个符合标准的编译器都允许使用。
Ahmet Ipkin '16

1
如果您有数十种方法怎么办?您可以只template class foo<int>;template class foo<std::string>;在.cpp文件的末尾执行操作吗?
无知

Answers:


231

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

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

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


39
只是为了补充答案,引用的链接肯定会回答问题,即可以执行Rob的建议并使代码具有可移植性。
ivotron

160
您可以在答案本身中发布相关部分吗?为什么在SO上甚至允许这样的引用。我不知道该链接中要查找的内容,因为此后已经发生了很大的变化。
等同

124

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

15
您的意思是“用于显式CLASS模板专业化”。在那种情况下,将覆盖模板化类具有的每个函数吗?
亚瑟

@Arthur似乎不行,我在头文件中保留了一些模板方法,而在cpp中大多数其他方法都可以正常工作。非常好的解决方案。
user1633272

对于询问者,它们具有功能模板,而不是类模板。
user253751

23

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

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


2
什么是不出口什么意思呢?
Dan Nissenbaum 2014年

1
@Dan仅在其编译单元内可见,而在其外部不可见。如果将多个编译单元链接在一起,则可以在它们之间使用导出的符号(并且必须具有单个符号,或者至少对于模板而言,符号必须具有一致的定义,否则会遇到UB)。
Konrad Rudolph 2014年

谢谢。我认为所有功能(默认情况下)在编译单元外部都是可见的。如果我有两个编译单元a.cpp(定义函数a() {})和b.cpp(定义函数b() { a() }),那么这将成功链接。如果我是对的,那么以上引用似乎不适用于典型情况……我在某处出错了吗?
Dan Nissenbaum 2014年

@Dan Trivial反例:inline函数
Konrad Rudolph

1
@Dan函数模板是隐式的inline。原因是没有标准化的C ++ ABI,很难/不可能定义否则会产生的效果。
Konrad Rudolph

15

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


13

您的示例是正确的,但不是很容易移植。还有一种更简洁的语法可以使用(由@ namespace-sid指出)。

假设模板化的类是要共享的某些库的一部分。是否应编译模板版本类的其他版本?库维护者是否应该预期该类的所有可能的模板化使用?

另一种方法是对您所拥有的内容稍作更改:添加第三个文件,即模板实现/实例化文件。

foo.h文件

// Standard header file guards omitted

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

foo.cpp文件

// Always include your headers
#include "foo.h"

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

foo-impl.cpp文件

// Yes, we include the .cpp file
#include "foo.cpp"
template class foo<int>;

一个警告是,您需要告诉编译器进行编译,foo-impl.cpp而不是foo.cpp因为编译后者没有任何作用。

当然,您可以在第三个文件中具有多个实现,也可以针对每种类型使用多个实现文件。

共享模板类以供其他用途时,这将提供更大的灵活性。

此设置还减少了重用类的编译时间,因为您没有在每个转换单元中重新编译相同的头文件。


这买了什么?您仍然需要编辑foo-impl.cpp来添加新的专业化名称。
MK。

foo.cpp实际从中编译版本(在中foo-impl.cpp)和声明(在中foo.h)的实现细节(也就是中的定义)分开。我不喜欢大多数C ++模板完全在头文件中定义。这与c[pp]/h使用的每个类/命名空间/任何分组的成对C / C ++标准相反。人们似乎仍在使用整体头文件,只是因为这种替代方法尚未广泛使用或未知。
卡梅隆·塔克林

1
@MK。首先,我将显式模板实例化放在源文件中定义的末尾,直到需要在其他地方进行进一步实例化(例如,使用模拟作为模板类型的单元测试)。这种分离使我可以在外部添加更多实例化。此外,当我将原始对象h/cpp成对保存时,它仍然可以工作,尽管我必须将原始实例化列表包含在一个includeguard中,但是我仍然可以foo.cpp像往常一样编译它们。我对C ++还是很陌生,并且想知道这种混合用法是否还有其他警告。
Thirdwater '18 / 12/28

3
我认为将foo.cpp和解耦比较好foo-impl.cpp。不要#include "foo.cpp"foo-impl.cpp文件中;而是添加声明extern template class foo<int>;foo.cpp防止编译器在进行编译时实例化模板foo.cpp。确保生成系统生成两个 .cpp文件,并将两个目标文件都传递给链接器。这有很多好处:a)很明显,foo.cpp没有实例化;b)对foo.cpp的更改不需要重新编译foo-impl.cpp。
Shmuel Levine,

3
这是一个很好的解决模板定义问题的方法,它同时兼顾了两个方面-头文件实现和常用类型的实例化。我会做这种设置的唯一变化是重命名foo.cppfoo_impl.hfoo-impl.cpp成刚foo.cpp。我还要补充的typedef的实例从foo.cppfoo.h同样using foo_int = foo<int>;。诀窍是为用户提供两个头接口供选择。当用户需要预定义的实例化时,他包括foo.h;当用户需要乱序的东西时,他包括foo_impl.h
Wormer

5

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


您确定它将打破ODR吗?如果TemplateClassInst.cpp中的实例化行引用的是相同的源文件(包含模板函数定义),难道不能保证不违反ODR,因为所有定义都相同(即使重复)吗?
Dan Nissenbaum 2014年

拜托,什么是ODR?
无法移除'18

4

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

有关此问题,请参见FAQ-lite


2
AFAIK,每次他们解决最后一个问题时,出口就死定了,因为他们正面临越来越多的问题,从而使整体解决方案变得越来越复杂。而且,无论如何,“ export”关键字都将使您无法从CPP“导出”(无论如何仍是从H. Sutter的)。所以我说:别屏住呼吸……
paercebal 08/09/22

2
为了实现导出,编译器仍然需要完整的模板定义。您所获得的只是以某种编译形式进行的。但是,实际上没有任何意义。
Zan Lynx 2012年

2
... 由于过于复杂以至于无法获得最大收益,因此已经偏离了标准。
DevSolar

4

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

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

  2. 在相同的标题中,但在类外部定义模板。在大多数情况下,这是我的首选方式。它使您的类定义保持整洁,模板仍然是真实的模板。但是,它需要完整的模板命名,这可能很棘手。另外,您的代码对所有人开放。但是,如果您需要代码内联,则这是唯一的方法。您也可以通过在类定义的末尾创建一个.INL文件来完成此操作。

  3. 将header.h和Implementation.CPP包含在main.CPP中。我认为就是这样。您无需准备任何预实例化,它的行为就像一个真正的模板。我的问题是它不是自然的。通常,我们不包含并且希望包含源文件。我想因为您已经包含了源文件,所以可以内联模板函数。

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

注意,我不知道膨胀的obj文件的后果。


3

是的,这是专业化的标准方法化显式实例化。如您所述,您无法使用其他类型实例化此模板。

编辑:根据评论更正。


对术语保持谨慎是一种“显式实例化”。
理查德·科登,

2

让我们举一个例子,由于某种原因,您想拥有一个模板类:

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


1

是时候更新了!创建一个内联文件(.inl或其他文件),然后在其中复制所有定义。确保在每个功能(template <typename T, ...>)上方添加模板。现在,与在内联文件中不包含头文件相反,您执行相反的操作。在声明类()包括内联文件#include "file.inl"

我真的不知道为什么没人提到这一点。我认为没有立即出现的弊端。


25
直接的缺点是,它与直接在标头中定义模板函数基本相同。一旦您完成#include "file.inl",预处理器将file.inl直接将的内容粘贴到标头中。无论您想避免在标头中进行实现的原因为何,此解决方案都无法解决该问题。
科迪·格雷

5
- 并且这意味着您在技术上不必要地负担着编写离线template定义所需的所有冗长且令人弯腰的重复样板的工作。我明白了为什么人们想要这样做-用非模板声明/定义实现最大的同等性,使接口声明看起来整洁等-但这并不总意味着麻烦。这是评估双方的权衡并选择最坏的情况的一种情况。...直到namespace class变成事物:O [ 请成为事物 ]
underscore_d

2
@Andrew尽管我认为我看到有人说这不是故意的,但似乎已经陷入了委员会的困境。我希望它已经成为C ++ 17。也许下个十年。
underscore_d

@CodyGray:从技术上讲,对于编译器而言确实是相同的,因此不会减少编译时间。我仍然认为这值得一提,并在我见过的许多项目中得到了实践。走这条路有助于将接口与定义分开,这是一个好习惯。在这种情况下,它对ABI兼容性等没有帮助,但是却简化了对接口的阅读和理解。
kialalphaindia

0

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

当与显式类实例一起使用时,Boost Concept Check Library(BCCL)可以帮助您在cpp文件中生成模板功能代码。


8
什么是低效的?
科迪·格雷

0

以上都不对我有用,所以这是您如何解决它,我的班级只有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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.