C++公共API的最佳实践是什么?

12

C++公共API的最佳实践是什么?

我正在开发一个C++项目,其中有多个命名空间,每个命名空间中都有多个对象。一些对象具有相同的名称,但位于不同的命名空间中。目前,每个对象都有自己的.cpp文件和.h文件。我不确定如何表达这个问题......创建第二个.h文件来仅公开公共API是否合适?对于每个命名空间或每个对象,应该有一个.h文件还是其他范围?对于创建C++库的公共API,可能的最佳实践是什么?

感谢任何帮助, Chenz

6个回答

2

有时候,在每个.cpp和.h文件对中都拥有一个单一的类,并将命名空间层次结构作为目录结构,这样做很方便。
例如,如果你有这个类:

namespace stuff {
  namespace important {
    class SecretPassword 
    {
       ...
    };
  }
}

接下来它将分为两个文件:

/stuff/important/SecretPassword.cpp
/stuff/important/SecretPassword.h

另一种可能的布局可能是:

/src/stuff/important/SecretPassword.cpp
/include/stuff/important/SecretPassword.h

但是,当将所有内容放在一个单独的.so库中时,我是否需要分发深层包含目录结构? - Crazy Chenz
@Crazy Chenz,是的,这是可能的。例如,在Linux内核中以及在某种程度上在boost中就是这样完成的。 - shoosh

2

你好,

我的建议是看一下C++的Handle-Body惯用语,有时也被称为Cheshire Cat。这里是James Coplien的原始论文,其中包含了这种惯用语。

这是一种著名的方法,可将公共API与实现分离。

希望能对你有所帮助。


3
这个习语的另一个名称(我认为更为流行,但我可能错了)是“Pimpl习语”-指向实现的指针。 - Michael Burr
@michael,你说得对。如果我没记错的话,这个名称也是最近才添加的。handle/body是Cope在他1991年的书《高级C++编程风格和习惯》中给它起的原始名称。http://www.amazon.com/dp/0201548550/ - Rob Wells

1

我认为最好由您自己决定,这取决于这是什么类型的“库”。

您的API提供一个“操作”吗?还是只处理一个抽象的“数据类型”?例如zlib和libpng。两者都只有一个头文件,可以提供执行库所用的所有内容。

如果您的库是一组不相关(甚至相关)的类,它们是否达到了相同的目标,请为每个子集提供自己的头文件。主要示例是boost。


1

这是我通常做的:

“有些对象具有相同的名称,但位于不同的命名空间中”

这就是为什么存在命名空间的原因。

“创建第二个.h文件来仅公开公共API是否合适?”

您应该始终仅公开公共API。但是公开公共API是什么意思?如果只是头文件,那么由于公共API依赖于私有API,私有API将被公共API包含。要公开公共API,请使用宏标记公共函数/类(在Windows情况下,将公共函数导出到符号表;可能很快会被Unix系统采用)。因此,您应该定义一个类似于MYLIB_API或MYLIB_DECLSPEC的宏,只需检查一些现有库和MS declspec文档即可。通常,非公共API将保留在子目录中,因此不会参加库的用户。

“每个命名空间或每个对象或其他范围应该有一个.h文件吗?”

我更喜欢Java风格,每个头文件只有一个public类。我发现以这种方式编写的库比混合文件和结构名称的库更清晰易读。但是在某些情况下,我会打破这个规则,特别是涉及模板时。在这种情况下,我会给出#warning消息,不要直接包含头文件,并在注释中仔细解释正在发生的事情。


“但是公开API的意思是什么?” 我想沿着复制所有标题以进行分发而不包括它们的私有方法和变量的方向思考。 - Crazy Chenz
我的意思是……没有它们的私有方法原型和私有变量声明。 - Crazy Chenz
在我看来,这没有意义。事实上,这会使您的库更难理解和维护。如果有人需要扩展您的类,那么他可能希望看到整个类结构,而不仅仅是公共和受保护成员。类定义应该明确地保留在一个头文件中。 - mip

0

非常好的回答LiraNuna。

您是为应用程序或库提供API吗?

应用程序API通常只提供查询应用程序状态或尝试更改该状态的方法。在这种情况下,您通常会在单独的头文件中有不同的接口声明。然后,您的对象将实现此接口以处理API请求。

库通常公开可重复使用的对象。在这种情况下,一般来说,您的API只是头文件中的公共方法。


我有一个核心库应该是可重用的,然后有几个命名空间包含所有使用核心的应用程序的对象和帮助程序。核心是我想要公开API的部分。 - Crazy Chenz
1
好的,我的建议是尽可能减少范围。如果我包含了“foo.h”,不要让我继承一堆我不关心的函数和对象。考虑STL-我使用#include <map>而不是<collections>。只要你的接口完整且定义明确,在客户端代码中有许多包含文件比膨胀更可取(在我看来)。 - Josh
编译器不能消除膨胀吗?在像gtk和Xlib这样的库中,您只需包含gtk/gtk.h或X11/Xlib.h即可。他们是在为更干净的代码而牺牲保守性吗? - Crazy Chenz
我想在这些情况下,我可以更具体一些,但是他们添加了一个包含膨胀代码的便利头文件,这使我不必了解其他所有内容。 - Crazy Chenz
呵呵...如果编译器能够消除“膨胀”,那么拥有一个#include <stl>可能会很不错。 - Crazy Chenz
显示剩余2条评论

0

我同意文档中所说的 - 一个文件只有一个类。99.9%的情况下都是如此!

另外,请考虑使用哪些文件名。即使它们包含的类可能在不同的命名空间中,但在不同目录中具有相同名称的多个头文件通常是一个坏主意。

特别是如果这是一个公共API,您可能不能控制库使用者定义的include路径,因此构建可能会找到错误的路径。调整include路径的顺序绝对不是我推荐的解决方案!

我们使用Namespace-Class.h的命名约定来明确标识文件中的类。


请问您如何处理长命名空间?比如说,您有一个类 mylib::graphics::formats::JpegLoader。那么头文件的名称是 Mylib_Graphics_Formats-JpegLoader.h 吗?使用这样的头文件会让人感到有些沮丧。在我看来,避免意外包含的最佳方法是强制要求库名称成为包含路径的一部分,在我们的例子中,包含将如下所示:#include <mylib/graphics/formats/JpegLoader.h>。只要在 <> 中有“mylib”,一切都会很好。 - mip
幸运的是,我们的命名空间并没有嵌套得那么深;只有一个命名空间,很少有两个,所以情况并不太糟糕。它为什么让人沮丧 - #include "Mylib-Graphics-Formats-JpegLoader.h"#include "mylib/graphics/formats/JpegLoader.h" 之间有什么区别?你仍然必须以某种方式输入完整的“路径”吗? - Steve Folly
如果您按照命名空间的目录结构,可以使用#include <mylib/graphics/formats/Mylib-Graphics-Formats-JpegLoader.h>,而在我的端上则是<mylib/graphics/formats/JpegLoader.h>。如果您将所有文件放在一个目录中,则也可以。 - mip
@doc: 哦,呸!:-) 我们使用平面目录树,将“结构”放入文件名中。这样可以避免在你的例子中有2个JpegLoader.h文件时产生歧义。我可以看到,如果你强制API的用户设置他们的包含路径指向mylib所在的位置,那么就不会产生歧义了。但是你不能总是保证用户会做正确的事情(或者你可以保证他们做错误的事情!)。 - Steve Folly

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