在编写模板化的C ++类时,通常有三个选择:
(1)将声明和定义放在标题中。
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f()
{
...
}
};
要么
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f();
};
template <typename T>
inline void Foo::f()
{
...
}
优点:
缺点:
- 接口和方法实现混合在一起。这只是“可读性”问题。有些人认为这是无法维持的,因为它与通常的.h / .cpp方法不同。但是,请注意,在其他语言(例如C#和Java)中这不是问题。
- 重建影响很大:如果您
Foo
以成员的身份声明新类,则需要添加foo.h
。这意味着更改Foo::f
头文件和源文件传播的实现。
让我们仔细研究一下重建影响:对于非模板C ++类,将声明放在.h中,将方法定义放在.cpp中。这样,当更改方法的实现时,只需重新编译一个.cpp。如果.h包含您所有的代码,则这对于模板类是不同的。看下面的例子:
// bar.h
#pragma once
#include "foo.h"
struct Bar
{
void b();
Foo<int> foo;
};
// bar.cpp
#include "bar.h"
void Bar::b()
{
foo.f();
}
// qux.h
#pragma once
#include "bar.h"
struct Qux
{
void q();
Bar bar;
}
// qux.cpp
#include "qux.h"
void Qux::q()
{
bar.b();
}
在这里,的唯一用法Foo::f
是inside bar.cpp
。但是,如果你改变的实施Foo::f
,既bar.cpp
和qux.cpp
需要重新编译。Foo::f
即使两个文件中的任何部分都没有Qux
直接使用中的任何内容,这两个文件中都存在生命的实现Foo::f
。对于大型项目,这很快就会成为问题。
(2)将声明放在.h中,并将定义放在.tpp中,并将其包括在.h中。
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f();
};
#include "foo.tpp"
// foo.tpp
#pragma once // not necessary if foo.h is the only one that includes this file
template <typename T>
inline void Foo::f()
{
...
}
优点:
- 使用非常方便(只需包含标题)。
- 接口和方法定义是分开的。
缺点:
此解决方案将声明和方法定义分为两个单独的文件,就像.h / .cpp一样。但是,此方法具有与(1)相同的重建问题,因为标头直接包含方法定义。
(3)将声明放在.h中,将定义放在.tpp中,但不要在.h中包括.tpp。
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f();
};
// foo.tpp
#pragma once
template <typename T>
void Foo::f()
{
...
}
优点:
- 减少重建影响,就像.h / .cpp分隔一样。
- 接口和方法定义是分开的。
缺点:
- 使用不便:将
Foo
成员添加到类中时Bar
,您需要将其包括foo.h
在标题中。如果调用Foo::f
.cpp,则还必须在其中添加foo.tpp
。
这种方法减少了重建影响,因为仅Foo::f
需要重新编译真正使用的.cpp文件。但是,这是有代价的:所有这些文件都需要包含foo.tpp
。从上面的示例开始,并使用新方法:
// bar.h
#pragma once
#include "foo.h"
struct Bar
{
void b();
Foo<int> foo;
};
// bar.cpp
#include "bar.h"
#include "foo.tpp"
void Bar::b()
{
foo.f();
}
// qux.h
#pragma once
#include "bar.h"
struct Qux
{
void q();
Bar bar;
}
// qux.cpp
#include "qux.h"
void Qux::q()
{
bar.b();
}
如您所见,唯一的不同是foo.tpp
in中的附加include bar.cpp
。这很不方便,并且根据您是否在类上调用方法看起来很丑陋,因此为该类添加第二个include。但是,您可以减少重建的影响:仅bar.cpp
当更改的实现时才需要重新编译Foo::f
。该文件qux.cpp
无需重新编译。
摘要:
如果实现一个库,通常不需要关心重建影响。库的用户抓住并使用它,并且库的实现在用户的日常工作中不会改变。在这种情况下,库可以使用方法(1)或(2),而选择哪种只是一个口味问题。
但是,如果您正在处理应用程序,或者正在处理公司的内部库,则代码会经常更改。因此,您必须关心重建影响。如果让开发人员接受其他包含,则选择方法(3)是一个不错的选择。