typedef 的头文件最佳实践

Posted

技术标签:

【中文标题】typedef 的头文件最佳实践【英文标题】:Header file best practices for typedefs 【发布时间】:2011-01-22 07:53:23 【问题描述】:

我在一个项目中广泛使用 shared_ptr 和 STL,这导致了像 shared_ptr< vector< shared_ptr<const Foo> > > 这样的过长、容易出错的类型(我偏爱 ObjC 程序员,其中长名称是常态,并且仍然太多了。)我相信,始终将其称为 FooListPtr 并记录命名约定“Ptr”表示 shared_ptr 而“List”表示 shared_ptr 的向量会更清楚。

这很容易 typedef,但它会导致标题令人头疼。我似乎有几个选择在哪里定义FooListPtr

Foo.h.这会缠绕所有的标头并造成严重的构建问题,因此它不是首发。 FooFwd.h(“转发头”)。这就是 Effective C++ 所建议的,基于 iosfwd.h。这是非常一致的,但维护两倍数量的标头的开销充其量似乎很烦人。 Common.h(将它们全部放在一个文件中)。这通过缠绕许多不相关的类型而扼杀了可重用性。您现在不能只拿起一个对象并将其移动到另一个项目。这不是首发。 typedef 的某种奇特的#define 魔法,如果它还没有被typedefed 的话。我一直不喜欢预处理器,因为我认为它让新人难以理解代码,但也许.... 使用向量子类而不是 typedef。这似乎很危险......

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

如果其他人想添加其他讨论选项,我已标记此社区 wiki。

【问题讨论】:

请问为什么这个问题是一个社区维基? @Konrad,如果还有其他建议,我建议将它们添加到列表中,以便以后的读者更容易看到将各种选项与答案分开的优点。也许社区 wiki 的使用方式不同? 经过更多研究后,我重新发现了我上次点击社区 wiki 时发现的东西,那就是我不是故意的……希望这次我吸取了教训。 【参考方案1】:

我正在编写一个听起来像是使用common.h 方法的项目。它非常适合该项目。

在预编译头文件中有一个名为ForwardsDecl.h 的文件,它简单地向前声明了所有重要的类和必要的类型定义。在本例中使用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,也没关系,因为它只是转发声明,如果你不使用它编译器不在乎。因此,该文件可以包含它所使用的所有项目中的类名称,并且对于特定项目是否缺少某些类并不重要。所需要的只是必要的完整声明头(例如ObjectA.h)包含在任何实际使用它们的 (.cpp) 文件中。

【讨论】:

这与我组织 C++ 库的方式非常相似。前向声明头文件的要点是它应该声明库中的所有类和指针类型。这样你只需要一个这样的标题,而不是每个类一个。有关详细信息,请参阅***.com/questions/3935183/…。【参考方案2】:

我会采用前向标头和common.h 标头的组合方法,该标头特定于您的项目,仅包含所有前向声明标头和任何其他常见且轻量级的内容。

您抱怨维护两倍数量的标头的开销,但我认为这应该不是太大的问题:转发标头通常只需要知道非常有限数量的类型(一个?),并且有时甚至不是完整的类型。

如果真的有那么多标题,您甚至可以尝试使用脚本自动生成标题(例如在SeqAn 中完成)。

【讨论】:

我主要处理多个项目使用的代码,因此几乎没有一个特定于单个项目的代码。单个“common.h”使多个项目很难重用组件。 @Konrad:SeqAn 使用 boost 和 std 库吗?你提到的脚本是做什么的? @Benoît:SeqAn 使用 STL,但(不幸的是,恕我直言)没有 Boost 库。至于脚本,它只是解析所有的头文件(但是,SeqAn 只是头文件)并生成一个包含所有导出类型和函数的前向声明的大文件。 我同意他们不使用 boost 库太糟糕了(尤其是对于图形算法!)。脚本在某种程度上是标准的吗? 我以前从未亲自看过剧本;老实说,它看起来像一团糟。但另一方面,它工作起来非常可靠,可以处理复杂的模板定义。它看起来好像可以重复使用或定制而没有太多麻烦。想看的话:svn.mi.fu-berlin.de/seqan/trunk/seqan/misc/build_forwards.py【参考方案3】:

+1 用于记录 typedef 约定。

Foo.h - 你能详细说明你遇到的问题吗? FooFwd.h - 我一般不会使用它们,只在“明显的热点”上使用。 (是的,“热点”很难确定)。 它不会更改 IMO 规则,因为当您引入 fwd 标头时,来自 foo.h 的关联 typedef 会移动到那里。 Common.h - 很适合小型项目,但不能扩展,我同意。 某种花哨的#define...请不要!... 使用向量子类 - 并没有让它变得更好。 不过,您可能会使用收容措施。

所以这里是初步建议(从其他问题修改......)

    标准类型头文件&lt;boost/shared_ptr.hpp&gt;&lt;vector&gt; 等可以进入项目的预编译头文件/共享包含文件。 这还不错。(我个人仍将它们包含在需要的地方,但除了将它们放入 PCH 之外,这还有效。)

    如果容器是一个实现细节,typedefs 去容器被声明的地方(例如,如果容器是私有类成员,私有类成员)

    关联类型(如FooListPtr)转到声明 Foo 的位置,如果关联类型是该类型的主要用途。这对于某些类型几乎总是正确的——例如。 shared_ptr.

    如果Foo 获得一个单独的前向声明头,并且关联的类型可以接受,它也会移动到 FooFwd.h。

    如果该类型仅与特定接口相关联(例如,公共方法的参数),则它会出现在那里。

    如果类型是共享的(并且不满足任何前面的条件),它将获得自己的标头。请注意,这也意味着拉入所有依赖项。

这对我来说感觉“显而易见”,但我同意它作为编码标准并不好。

【讨论】:

一般来说好点,但关于 #1:我不建议在一个公共头文件中包含大量标准库。最严重的问题是产生的私有静态数据和定义的数量。 @Justin - 定义 - 同意,但这就是预编译头文件的用途。但是私有静态数据?标准标题中几乎没有(因为无论如何它都是每个翻译单元)。还是您的意思是早期模板支持实现的问题?除非这是您的编译器已知且经过验证的问题,否则坚持下去是没有意义的。 我不使用预编译的头文件。理由:没有足够的通用性,而且我经常使用组合构建,所以在很多情况下每个包编译一个文件。此外,预编译的头文件在分布式构建系统中的扩展性不是特别好。就静态数据而言,一个明显的声明是输入/输出流(包含在iostream 中)。 从标准库中删除多余的包含后,我丢失了大约 20% 的二进制文件(使用苹果 gcc 附带的相对现代的 std 库实现来测量)。这些问题显然超出了二进制大小。【参考方案4】:

我在一个项目中广泛使用 shared_ptr 和 STL,这导致了像 shared_ptr > 这样的过长、容易出错的类型(我是一个 ObjC 程序员,其中长名称是规范,但仍然太过分了。)我相信,始终将其称为 FooListPtr 并记录命名约定“Ptr”表示 shared_ptr 而“List”表示 shared_ptr 的向量会更清楚。

对于初学者,我建议使用良好的设计结构来确定范围(例如命名空间)以及类型定义的描述性、非缩写名称。 FooListPtr 太短了,imo。没有人想猜测缩写是什么意思(或者惊讶地发现 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。这是非常一致的,但是维护两倍数量的标头的开销充其量似乎很烦人。

真的不用担心这个维护。这是一个很好的做法。编译器毫不费力地使用前向声明和 typedef。这并不烦人,因为它有助于减少您的依赖关系,并有助于确保它们都是正确且可见的。由于其他文件引用了“包类型”标头,因此确实不需要维护更多内容。

Common.h(将它们全部放在一个文件中)。这通过缠绕许多不相关的类型而扼杀了可重用性。您现在不能只拿起一个对象并将其移动到另一个项目。这不是首发。

基于包的依赖和包含非常好(非常理想,真的)——不要排除这一点。您显然必须创建设计和结构良好的包接口(或库),并表示相关的组件类。您正在从对象/组件重用中提出不必要的问题。最小化库的静态数据,让链接和剥离阶段完成它们的工作。同样,保持你的包小且可重复使用,这不会是一个问题(假设你的库/包设计良好)。

typedef 的某种花哨的#define 魔法,如果它还没有被 typedefed 的话。我一直不喜欢预处理器,因为我认为它让新人难以理解代码,但也许....

实际上,您可以在同一范围内多次声明 typedef(例如,在两个单独的标头中)——这不是错误。

在同一范围内声明具有不同基础类型的 typedef 错误。明显地。你必须避免这种情况,幸运的是编译器强制执行。

为避免这种情况,请创建一个包含世界的“翻译构建” - 编译器将标记不匹配的类型定义类型的声明。

试图用最少的 typedef 和/或 forwards(它们足够接近编译时释放)偷偷摸摸是不值得的。有时你需要一堆对前向声明的条件支持——一旦定义,就很容易(stl 库就是一个很好的例子——如果你也前向声明template&lt;typename,typename&gt;class 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;
;

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

最终,我发现了一种基于小型简洁包的方法,最适合重用、减少编译时间和最小化依赖性。

【讨论】:

【参考方案5】:

不幸的是,对于 typedef,您必须在头文件的不理想选项之间进行选择。在某些特殊情况下,选项一(就在类标题中)效果很好,但听起来它对你不起作用。在某些情况下,最后一个选项效果很好,但通常是使用子类来替换涉及具有 std::vector 类型的单个成员的类的模式。对于您的情况,我将使用前向声明标头解决方案。有额外的打字和开销,但它不会是 C++,对吧?它使事情分开、干净、快速。

【讨论】:

这似乎是最一致和可扩展的方法。 “否则它不会是 C++ ......” 似乎确实如此。我通常是一名 ObjC 程序员。人们抱怨 ObjC 中的长名称,但我觉得它们很舒服,根本不介意它们。但是在 C++ 中,我觉得我一半的时间都花在了繁琐的工作上,或者为了避免额外的输入而使代码不一致且难以维护。

以上是关于typedef 的头文件最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

默认成员值最佳实践

Helm(三)—最佳实践

除了 syslog 之外,Linux 服务/守护程序文件记录最佳实践是啥? [关闭]

GWT 上传/下载最佳实践

存储分隔文本文件架构的最佳实践

CakePHP 上传文件的最佳实践