使用自定义数据类型定义接口:如何保持此可读性?

时间:2014-02-21 21:50:53

标签: c++ namespaces readability

我正在开发一个有点复杂的库。它提供了一堆使用一些自定义DLL_EXPORT的{​​{1}} ed函数。目前,这些结构是使用它们的函数定义的,如下所示:

struct

出于可维护性的目的,我逐渐将该库切换为更标准的界面风格:

struct MyDataType
{
  std::wstring name;
  std::wstring purpose;
}

DLL_EXPORT int DoSomething(MyDataType instruction);

为了使界面更简单,我正在使用第三方库。这个库要求我为我使用的每个自定义数据类型做一些设置工作,这是我不确定如何继续的地方。我看到了几种不同的方式:

方法1

struct MyLibraryInterface
{
  virtual int DoSomething(MyDataType instruction) = 0;
}

优点:

  • 任何需要使用我的lib的人都必须//MyLibraryInterface.h namespace MyLibraryInterface_v1_Types { struct MyDataType { std::wstring name; std::wstring purpose; } } #include "MyLibraryInterface_v1_Types_Backend.private.h" //this header would contain the required "paperwork" to make my datatype work with the 3rd-party lib struct MyLibraryInterface_v1 { virtual int DoSomething(MyDataType instruction) = 0; } 一个标题

  • 自定义命名空间使数据类型与函数定义分开

缺点:

  • 标题中间的#include看起来很邋and且不合适(虽然我注意到某些MS标题使用了这种技术)

  • 自定义命名空间是否真的必要,或者我只是让用户感到困惑? (封装本身肯定是必要的,因为我可能会改变这种数据类型的定义。但我不知道我是否需要一个自定义命名空间,或者我是否应该将数据类型声明放在结构的声明中使用它。)

方法2

#include

优点:

  • 在我的标题中途没有看起来很乱的//MyLibraryInterface.h namespace MyLibraryInterface_v1_Types { struct MyDataType { std::wstring name; std::wstring purpose; } } //3rd-party "paperwork" is directly placed here struct MyLibraryInterface_v1 { virtual int DoSomething(MyDataType instruction) = 0; }

缺点:

  • 第三方文书工作现在显示在我的用户将使用的标题中(他们不需要看到它,我宁愿他们没有,出于化妆/易于理解的原因)

  • 这感觉就像我没有充分利用数据类型命名空间的功能,因为第三方代码在我的库代码中是自由浮动的而不是封装的

方法3

#include

优点:

  • 删除自定义命名空间
  • 减少混淆,因为用户现在指的是//MyLibraryInterface.h struct MyLibraryInterface_v1 { struct MyDataType { std::wstring name; std::wstring purpose; } virtual int DoSomething(MyDataType instruction) = 0; } #include "MyLibraryInterface_v1_Types_Backend.private.h" //this header would contain the required "paperwork" to make my datatype work with the 3rd-party lib 而不是MyLibraryInterface_v1::MyDataType,如果他们在MyLibraryInterface_v1_Types::MyDataType调用函数
  • ,则会更直观

缺点:

    标题最底部的
  • MyLibraryInterface_v1看起来真的不好

  • 混合数据类型和函数声明对我来说似乎有些不确定

方法4

#include

优点:

  • 第三方文书工作直接涉及需要它的数据类型的声明

缺点:

  • 用户可能很难找到或使用自定义数据类型

  • 感觉非常不直观,因为数据类型同时驻留在单独的标头和单独的命名空间中


哪个最好?我完全忽略了一种不同的,更好的方法吗?或者我只是必须咬紧牙关并接受这一点,无论我决定采用哪种方式,我都会遇到一些问题。


使用更多信息进行更新:

我正在使用的第三方库将我的界面包裹在//MyLibraryInterface_v1_Types.h namespace MyLibraryInterface_v1_Types { struct MyDataType { std::wstring name; std::wstring purpose; } } //3rd-party paperwork can be directly placed here, immediately following the definition of the custom datatype //MyLibraryInterface.h #include "MyLibraryInterface_v1_Types.h" /* this header, as defined above, holds the definitions of the custom datatypes this library will use. It also includes the 3rd-party paperwork required to make those datatypes work. It can't be a private header, though, because users will need to access it to use the custom types. */ struct MyLibraryInterface_v1 { virtual int DoSomething(MyDataType instruction) = 0; } 中。所以我将能够创建struct的对象,第三方库将允许我从指定的DLL访问该接口的实现,然后我可以调用MyLibraryInterface*。这基本上是pImpl的变体。

此第三方库还会自动包装任何STL类型和任何自定义数据类型,以便可以在多个编译器中使用它们,因此我的MyLibraryObj->DoSomething()用法在这里是完全安全的。但是,该库要求我为 包装自定义类型提供某些设置信息。在定义每个自定义类型之后,我必须在某处提供设置信息,这会阻止将std::wstring与私有设置信息放在我的接口标头顶部的“正常”模式。我也无法完全从接口头中删除私有设置信息;任何通过此接口调用我的库的人都必须使用第三方库来执行此操作,并且他们需要再次提供接口的声明,以便库知道它在给定的DLL中寻找什么。我所能做的就是尽量让私人设置工作看起来尽可能整洁,并且理想地将其标记为我的图书馆用户永远不需要或想要直接使用的东西。

此外,我可以选择将自定义数据类型放入接口#include或自己的struct。我一开始就把它们直接放在namespace中玩,但由于其中一些数据类型是常量数据(struct es),​​因此将它们放入enum class中似乎有点草率功能声明。一个struct“觉得”更干净,但功能和数据类型的不同之处在于(namespace vs myLibraryObj->DoSomething()),因此可能不如保持{{1 (MyLibraryInterface_v1_Types::MyDataTypestruct)。

3 个答案:

答案 0 :(得分:1)

我认为你不应该有一个单独的命名空间来保存你的库的类型。作为用户,我喜欢这样,当使用库NiftyLibrary时,它的所有实体(即类型,函数等)都包含在名为Nifty的名称空间中或类似的名称空间中。当然,如果库太大,这个命名空间又可以包含其他命名空间,但是你明白了。我发现在使用NiftyLibrary类型之一时引用名称空间NiftyTypes并且在Nifty时引用它是奇怪的。可能是类型的标识符与函数的标识符发生碰撞,但后来你做了一些根本错误的事情。

是否绝对是否需要公开您向最终用户提及的第三方库等实施细节?你必须考虑到,当他们#include你的标题时,他们也将#include那些,所以编译性能会降低,最重要的是,一些标识符使用的标识符用户可能会与第三方库使用的用户发生冲突。另一种方法是将所有后者声明在detail命名空间内,就像Boost的库一样。不过,在我看来,这不是最干净的选择。你不能从头文件中移出与自定义数据类型相关的设置吗?它可能不可能,如果它需要实际做类型的东西,但给它一个镜头。 (你听说过PIMPL成语吗?)

答案 1 :(得分:1)

如果您正在为其他人创建一个库,请始终将其包装在命名空间中。使其足够长,以便完全描述。如果有人想要在限定名称中使用较短的名称,他们可以为自己的用途定义名称空间别名。此外,您无需担心内部标题的样子。如果你在文档方面做得很好,没有人需要查看标题。

命名空间可以嵌套,您可以使用它来屏蔽(但不隐藏)实现细节。一个经常使用的约定是调用这样的命名空间detail。编写文档,指出此命名空间不供公众使用,并包含可能更改的详细信息。

回想一下,#include是纯粹的文本机制,只需用一个文本块替换该指令即可。因此,如果在detail命名空间中包含外部标头,则它不会出现在全局命名空间中,也不会出现在顶级库命名空间中。通过这种方式包含外部定义,您可以只显示外部标头所需的内容;其他一切都在detail内屏蔽。

以下示例说明了这些原则。您可以暂停怀疑,并假设在某个外部头文件中定义了external_library。该示例说明了上面概述的每个原理。我假设您需要外部库作为某些类型定义的一部分;如果没有,它根本不应该在标题中。

namespace library {
    namespace detail {
        #include <whatever>
        namespace external_library {
            class exposed {} ;
            class hidden {} ;
        }
    }
    typedef detail::external_library::exposed external_type ;
    class my_type {} ;
}
library::my_type foo ;
library::external_type bar ;

我没有解决您提出的外部链接问题,因为它们与您问题的核心范围问题是分开的。

答案 2 :(得分:0)

我会应用下一个图书馆开发规则:

  1. 使用命名空间是一个好主意,使其更短。我建议不要超过5个字母。这样,如果图书馆的消费者具有相同的类型名称(我在我的carreer中遇到过这种情况),他将能够区分你的类型和他的类型。
  2. 将所有定义放在该命名空间下 - types + interfaces。
  3. 不要在公共标题中包含私有标头。在源文件中包含私有标头。
  4. 在您的接口(wstring)中使用STL将限制库的客户端使用相同的编译器(VC或gcc)及其相同版本。 一个。我会检查您的客户端将使用哪个编译器,并将此编译器用于您的库。你有多少客户?他们是大企业还是小企业? 湾另一种选择 - 提供两个版本的库 - VC和gcc。 C。如果您将来必须支持太多编译器类型和版本 - 更好地使用普通类型 - wchar_t数组(但我认为这是最不喜欢的选项)