使用头文件和源文件的模板化类

Templated Classes using header and source file

本文关键字:源文件 文件      更新时间:2023-10-16

经过大量的研究,我明白了为什么模板类传统上不能分为头文件和源文件。

然而,这(为什么模板只能在头文件中实现?)似乎暗示你仍然可以有一种伪分离编译过程,通过在头文件的末尾包括实现文件,如下所示:

// Foo.h
template <typename T>
struct Foo
{
    void doSomething(T param);
};
#include "Foo.tpp"
// Foo.tpp
template <typename T>
void Foo<T>::doSomething(T param)
{
    //implementation
}

的解释是:"一个常见的解决方案是在头文件中编写模板声明,然后在实现文件(例如。tpp)中实现类,并将该实现文件包含在头文件的末尾。"

然而,当我这样做时,我得到多个错误,说明它是一个构造函数/方法/函数的重新定义。我已经能够通过在.cpp文件上放置include守卫来防止这种情况,这似乎是一种糟糕的做法。

我的主要问题是:

  1. 什么是正确的方法来做到这一点与实现包括在头文件的底部?

  2. 我的。cpp文件上的包含保护是否会阻止编译器为其他类型创建模板类/函数,因为它现在只能包含一次?

  3. 首先使用头文件的部分原因是为了防止每次包含代码时都被复制,以保持较短的编译时间?那么,模板化函数(因为它们必须在头文件中定义)与简单地重载函数/类的性能影响是什么?什么时候应该使用它们?

下面是我自己的简单Node结构体代码的删节版本:

// Node.hpp
#ifndef NODES_H
#define NODES_H
#include <functional>
namespace nodes
{
    template<class Object>
    struct Node
    {
    public:
        Node(Object value, Node* link = nullptr);
        void append(Node tail);
        Object data;
        Node* next;
    };
    template<class Object> void prepend(Node<Object>*& headptr, Node<Object> newHead);
}
// Forward 'declaration' for hash specialization
namespace std
{
    template <typename Object>
    struct hash<nodes::Node<Object>>;
}
#include "Node.cpp"
#endif
// Node.cpp
// #ifndef NODE_CPP
// #define NODE_CPP
#include "Node.hpp"
template<class Object>
nodes::Node<Object>::Node(Object value, Node* link): data(value), next(link) {}
template<class Object>
void nodes::Node<Object>::append(Node tail) {
    Node* current = this;
    while (current->next != nullptr) {
        current = current->next;
    }
    current->next = &tail;
}
template<class Object>
void nodes::prepend(Node<Object>*& headptr, Node<Object> newHead) {
    Node<Object>* newHeadPtr = &newHead;
    Node<Object>* temporary = newHeadPtr;
    while (temporary->next != nullptr) {
        temporary = temporary->next;
    }
    temporary->next = headptr;
    headptr = newHeadPtr;
}
namespace std
{
    template <typename Object>
    struct hash<nodes::Node<Object>>
    {
        size_t operator()(nodes::Node<Object>& node) const
        {
            return hash<Object>()(node.data);
        }
    };
}
// #endif

在头文件的底部包含实现的正确方法是什么?

将include警卫放入头文件,包括实现#include指令:

#ifndef __FOO_H
#define __FOO_H
// Foo.h
template <typename T>
struct Foo
{
    void doSomething(T param);
};
#include "Foo.tpp"
#endif

你也可以在Foo.tpp中添加守卫,但在你发布的情况下,它将没有多大意义。

我的。cpp文件上的包含保护是否会阻止编译器为其他类型创建模板类/函数,因为它现在只能包含一次?

通常你根本不需要在*.cpp文件中包含守卫,因为你不会在任何地方包含它们。只需要在包含在多个翻译单元中的那些文件中使用Include警卫。当然,这些保护不会阻止为其他类型实例化模板,因为这是模板设计的目的。

首先使用头文件的部分原因不是为了防止每次包含代码时都被复制,以保持较短的编译时间吗?那么,模板化函数(因为它们必须在头文件中定义)与简单地重载函数/类的性能影响是什么?什么时候应该使用它们?

你在这里提出了一个大的、平台相关的、有点基于观点的话题。历史上,include文件用于防止代码复制,正如您所说的。将函数声明(头文件,而不是定义)包含到多个翻译单元中,然后将它们与所包含函数的编译代码的单个副本链接起来就足够了。

模板的编译速度比非模板函数慢得多,所以实现模板导出(单独的模板头/实现编译)不值得节省编译时间。

关于模板性能的一些讨论和好的答案,请检查这些问题:

  • 模板元编程比等效的C代码更快吗?
  • c++模板的性能?
  • c++模板会使程序变慢吗?

简而言之,有时模板允许您在编译时而不是运行时做出一些决定,从而使代码更快。无论如何,确定代码是否变得更快的唯一正确方法是在实际环境中运行性能测试。

最后,模板更多的是关于设计,而不是性能。它们允许您显著减少代码重复并符合DRY原则。一个普通的例子是std::max这样的函数。一个更有趣的例子是Boost。