头文件中typedef的最佳实践

53
我在一个项目中广泛使用shared_ptr和STL,这导致出现过长、容易出错的类型,例如shared_ptr< vector< shared_ptr<const Foo> > >(我倾向于ObjC编程,在那里长名称是常态,但这个太多了)。我认为更清晰的做法是一贯地称之为FooListPtr 并记录命名约定,“Ptr”表示shared_ptr,“List”表示shared_ptr所包含的vector。
这很容易进行typedef,但它会引起头文件的问题。我似乎有几个选项来定义FooListPtr
  • Foo.h. 这会缠绕所有头文件并创建严重的构建问题,因此它不可行。
  • FooFwd.h(“前向头文件”)。这是根据iosfwd.h建议的方法。它非常一致,但维护两倍数量的头文件的额外开销最多也很麻烦。
  • Common.h(将所有内容放在一个文件中)。这通过缠绕许多不相关的类型来杀死重用性。现在您不能只拿起一个对象并将其移动到另一个项目。这是不可行的。
  • 某种复杂的#define魔法,如果尚未进行typedef则对其进行typedef。我对预处理器有着深深的不喜欢,因为我认为它使新手很难理解代码,但也许....
  • 使用vector子类而不是typedef。这似乎很危险......
有最佳实践吗?当重用性、可读性和一致性至关重要时,它们在实际代码中的表现如何?
如果其他人想要添加额外的选项进行讨论,我已将此标记为社区wiki。

5
请问为什么这个问题是社区维基? - Konrad Rudolph
@Konrad,如果有其他提议,我建议将它们添加到列表中,以便后来的读者可以更容易地看到各种选项,而不是从答案本身的价值来考虑。也许社区维基使用方式不同? - Rob Napier
1
经过更多的研究,我重新发现了上次点击社区Wiki时发现的内容,即我并不是想这样做...希望这次我能吸取教训。 - Rob Napier
5个回答

15

我正在开发一个项目,听起来它使用了common.h方法。对于该项目,这种方法非常有效。

有一个名为ForwardsDecl.h的文件位于预编译头文件中,它简单地前向声明所有重要类和必要的typedefs。在这种情况下,使用 unique_ptr 而不是 shared_ptr,但使用方式应该是相似的。它看起来像这样:

// Forward declarations
class ObjectA;
class ObjectB;
class ObjectC;

// List typedefs
typedef std::vector<std::unique_ptr<ObjectA>> ObjectAList;
typedef std::vector<std::unique_ptr<ObjectB>> ObjectBList;
typedef std::vector<std::unique_ptr<ObjectC>> ObjectCList;

即使这些类只被前向声明(不需要包含每个类的头文件),Visual C++ 2010也接受此代码。我不知道是否标准,其他编译器是否需要完整的类定义,但它很有用:另一个类(ObjectD)可以将ObjectAList作为成员,而无需包括ObjectA.h- 这可以真正帮助减少头文件依赖!

维护并不是特别重要,因为前向声明只需要编写一次,并且任何后续更改只需要在类头文件中进行完整声明(由于降低了依赖性,这将触发较少的源文件重新编译)。

最后,似乎这可以在项目之间共享(我自己没有尝试过),因为即使一个项目实际上没有声明ObjectA,也没有关系,因为它只是前向声明,如果你不使用它,编译器也不关心。因此,该文件可以包含所有使用它的项目中的类名称,如果某些类在特定项目中缺失也没有关系。所有需要的是在任何使用它们的(.cpp)文件中包含必要的完整声明头(例如ObjectA.h)。


这与我组织C++库的方式非常相似。前向声明头文件的重点是应该在库中声明所有类和指针类型。这样,您只需要一个这样的头文件,而不是每个类都需要一个。有关更多详细信息,请参见https://dev59.com/SG865IYBdhLWcg3wM7u0#3935468。 - Daniel Lidström

6
我建议采用前向声明和一种特定于您的项目的common.h头文件的组合方法,该头文件仅包括所有前向声明头文件和任何其他通用且轻量级的内容。
您抱怨维护两倍数量的头文件的开销,但我认为这不应该是太大的问题:前向声明头文件通常只需要知道非常有限的类型(一个?),有时甚至不需要完整的类型。
如果确实存在许多头文件,您甚至可以尝试使用脚本自动生成头文件(例如在SeqAn中执行此操作)。

我主要负责编写被多个项目使用的代码,所以几乎没有任何内容是针对单个项目的。一个单独的“common.h”会给多个项目重用组件带来很大困难。 - Rob Napier
@Konrad :SeqAn 是否使用 boost 和 std 库?你提到的脚本是做什么用的? - Benoît
@Benoît:SeqAn使用STL,但(在我看来)不幸的是没有Boost库。至于脚本,它只是解析所有头文件(但是,SeqAn仅包含头文件),并生成一个包含所有导出类型和函数的前向声明的大文件。 - Konrad Rudolph
我同意他们不使用Boost库(尤其是图算法)的做法很糟糕!这个脚本在某种程度上是否标准? - Benoît
我之前从未亲自查看过这个脚本;老实说,它看起来很混乱。但另一方面,它非常可靠,并且能够处理复杂的模板定义。而且看起来它可以在不太费力的情况下被重用或定制化。如果你想看一下:http://svn.mi.fu-berlin.de/seqan/trunk/seqan/misc/build_forwards.py - Konrad Rudolph

4

+1 记录 typedef 约定。

  • Foo.h - 你能详细说明一下你对它的问题吗?
  • FooFwd.h - 我通常不使用它们,只用于“明显的热点”。(是的,“热点”很难确定。)这并不改变规则,因为当你引入一个 fwd 头文件时,来自 foo.h 的相关 typedefs 就会移动到那里。
  • Common.h - 对于小型项目很酷,但不具有可扩展性,我同意。
  • 某种花哨的 #define... 请不要!...
  • 使用 vector 子类 - 并不能让它更好。不过,你可以使用包含关系。

所以这里是初步建议(从另一个问题中修订而来...)

  1. 标准类型头文件 <boost/shared_ptr.hpp><vector> 等可以放入项目的预编译头文件/共享 include 文件中。这不是坏事。(我个人仍然在需要时包含它们,但这是除了将它们放入 PCH 中之外的工作。)

  2. 如果容器是实现细节,则 typedefs 放在声明容器的地方(例如,如果容器是私有类成员,则放在私有类成员中)

  3. 关联类型(如 FooListPtr)应放在声明 Foo 的地方,如果关联类型是类型的主要用途。对于某些类型来说,这几乎总是正确的 - 例如 shared_ptr

  4. 如果 Foo 获得一个单独的前向声明头文件,并且相关类型与此兼容,则它也会移动到 FooFwd.h 中。

  5. 如果该类型仅与特定接口相关联(例如公共方法的参数),则将其放在那里。

  6. 如果该类型是共享的(并且不符合上述任何标准),则它将获得自己的头文件。请注意,这也意味着拉入所有依赖项。

对我来说,这感觉很“明显”,但我同意它不适合作为编码规范。


总体来说是好的建议,但是关于第一点:我不建议在一个公共头文件中包含大量标准库。最糟糕的问题是会产生大量私有静态数据和定义。 - justin
@Justin - 定义 - 没错,但这就是预编译头文件的作用。但私有静态数据呢?标准头文件中几乎没有(因为它会按翻译单元进行)。或者你是指早期模板支持实现的问题?除非这是你的编译器已知且经过验证的问题,否则坚持使用它是没有意义的。 - peterchen
我不使用预编译头文件。原因是:共同性不足,而且我经常使用组合构建,因此在许多情况下每个软件包编译一个文件。而且,预编译头文件在分布式构建系统中的可扩展性并不尤其好。至于静态数据,一个明显的声明就是输入/输出流(由“iostream”包含)。在从标准库中删除多余的包含后,我测量了一下某些二进制文件的大小,结果损失了约20%(这些标准库是使用苹果的gcc提供的相对较新的实现)。问题显然不止二进制文件大小。 - justin

3
我在一个项目中广泛使用shared_ptr和STL,这导致类型过长、容易出错,例如shared_ptr< vector< shared_ptr > >(我偏爱ObjC编程,在那里长名称是常见的,但即便如此,这也太多了)。我认为,要更清晰,最好一致地称其为FooListPtr,并记录命名惯例,“Ptr”表示shared_ptr,“List”表示vector of shared_ptr。
首先,我建议使用良好的设计结构来进行作用域限定(例如命名空间),并为typedef选择具有描述性、非缩写的名称。我认为FooListPtr太短了。没有人想猜测缩写的含义(或者惊讶地发现Foo是const、shared等),也没有人想仅仅因为作用域冲突而修改自己的代码。
在您的库(以及其他常见类别)中选择typedef的前缀可能也有所帮助。
将类型拖出其声明的作用域也是一个坏主意:
namespace MON {
namespace Diddy {
class Foo;
} /* << Diddy */

/*...*/
typedef Diddy::Foo Diddy_Foo;

} /* << MON */

这有一些例外情况:

  • 完全封装的私有类型
  • 新作用域内的包含类型

顺便说一下,在命名空间作用域和命名空间别名中使用“using”应该被避免 - 如果想要最小化未来维护,请限定作用域。

虽然这很容易typedef,但它会造成头文件的问题。我似乎有几个选项可以定义FooListPtr:

Foo.h。这会交错所有头文件,并创建严重的构建问题,因此不可行。

对于确实依赖于其他声明的声明可能是一个选项,暗示您需要划分软件包,或者子系统有一个公共的本地化接口。

FooFwd.h(“前向标头”)。Effective C++基于iosfwd.h建议使用此方法。这非常一致,但是维护两倍数量的标头的开销似乎最多令人讨厌。

真的不用担心这个维护问题。这是一个好习惯。编译器使用前向声明和typedefs代价微小。它并不烦人,因为它有助于减少依赖关系,并确保它们都是正确和可见的。其他文件引用“包类型”头文件,因此实际上没有更多要维护的了。

Common.h(将所有内容放在一个文件中)。这通过交叉编织许多无关类型来破坏了可重用性。现在你不能只拿起一个对象并将其移动到另一个项目中。这是不可行的。

基于软件包的依赖关系和包含方式非常好(实际上是理想的)-不要排除这一点。显然,您必须创建设计良好、结构良好且表示相关类组件的包接口(或库)。你正在为对象/组件重用制造一个不必要的问题。最小化库的静态数据,并让链接和剥离阶段完成它们的工作。再次强调,保持您的软件包小而可重用,这将不成问题(假设您的库/软件包经过良好的设计)。

某种花哨的#define魔术,如果没有typedefed就typedef。我对预处理器有一种深深的厌恶,因为我认为它使新手难以理解代码,但也许....

实际上,您可以在同一作用域内多次声明typedef(例如,在两个不同的标头文件中)-这不是错误。

在同一作用域内声明具有不同基础类型的typedef 错误的。显然。您必须避免这种情况,幸运的是编译器会强制执行此操作。

为了避免这种情况,请创建一个“翻译构建”,其中包括全局信息-编译器将标记不匹配的typedef类型声明。

试图通过最小化typedef和/或前向声明(在编译时足够接近免费)来偷懒是不值得的。有时你需要一堆条件支持前向声明 - 一旦定义了,就很容易了(STL库是一个很好的例子 - 在这种情况下,如果你也在前向声明template class vector;)。
最好让所有这些声明可见,以立即捕获任何错误,并且在这种情况下可以避免使用预处理器,这是一个额外的奖励。

使用vector子类而不是typedef。这似乎很危险......

std::vector的子类经常被标记为“初学者的错误”。此容器不应被子类化。不要仅仅为了减少编译时间/依赖性而采用不良实践。如果依赖性真的如此重要,你应该考虑使用PIMPL:
// <package>.types.hpp
namespace MON {
class FooListPtr;
}

// FooListPtr.hpp
namespace MON {
class FooListPtr {
    /* ... */
private:
    shared_ptr< vector< shared_ptr<const Foo> > > d_data;
};
}

这里有最佳实践吗?当复用性、可读性和一致性至关重要时,它们在实际代码中的表现如何呢?

最终,我发现一个基于小巧简洁的包的方法最适合复用,可以减少编译时间,同时最小化依赖。


1

很遗憾,使用typedef时,您必须在头文件中选择不太理想的选项。有一些特殊情况下,选项一(直接在类头文件中)效果很好,但听起来对您不适用。还有一些情况下,最后一个选项效果很好,但通常是在您使用子类替换涉及std :: vector类型单个成员的类模式时。对于您的情况,我建议使用前向声明头文件解决方案。这会增加一些输入和开销,但如果不这样做,那就不是C ++了,对吧?它可以保持事物分离,干净和快速。


这似乎是最一致和可扩展的方法。“否则就不会是C++了……”这似乎是正确的方式。我通常是一个ObjC程序员。人们抱怨ObjC中的长名称,但我觉得它们非常舒适,完全不介意。但在C++中,我感觉自己花了一半的时间做冗长的繁琐工作,或者为了避免额外的打字而使代码不一致和难以维护。 - Rob Napier

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接