C++ 命名空间,与 Java 包的比较

Posted

技术标签:

【中文标题】C++ 命名空间,与 Java 包的比较【英文标题】:C++ Namespaces, comparison to Java packages 【发布时间】:2011-01-07 16:01:25 【问题描述】:

我最近完成了一堆 Java 编码,并且已经习惯了非常具体的包命名系统,例如深度嵌套。 com.company.project.db。这在 Java、AS3/Flex 和 C# 中运行良好。我也看到在 C++ 中应用了相同的范例,但我也听说将 C++ 命名空间视为 Java 包的直接对应物是不好的。

这是真的吗,为什么?命名空间/包有什么相似之处和不同之处?如果您使用深度嵌套的命名空间,可能会出现什么问题?

【问题讨论】:

与任何其他语言(如 Java、AS3、C# 等)相同...我不明白您的问题的重点。 我的意思是我看不到任何东西,我很想知道一个明显的粉丝觉得它们有什么用处。 @Neil:它提供了一种很好的分层结构代码的方法。它可能还没有在 C++ 中流行起来(还没有!),但它非常有帮助。与平面命名空间相比,更细粒度的组织还有助于防止名称冲突。命名空间/包引入了比单独通过头文件实现的更好的模块化。 虽然我在自己的代码中使用了命名空间,但我遇到的情况非常少,它们实际上确实可以防止名称冲突,因此不需要复杂的层次结构来解决这个问题。而且我认为层次结构在任何情况下都不是一个非常有用的设计工具——如果我们从 OOP 中学到了什么,那就是复杂的层次结构不是答案。 @Neil,“我遇到的情况非常少,它们确实可以防止名称冲突”——我同意命名空间的 层次结构——但在工作之后最近使用 C 和 Objective-C 时,我痛苦地提醒我,当您完全 缺少命名空间时,这是一个多么大的问题!给所有东西一个前缀的“解决方案”是丑陋和不自然的,恕我直言,以及哪些命名空间适合替换。 【参考方案1】:

Java 包不是嵌套的,它们是扁平的。任何明显的嵌套只不过是一种命名约定。

例如,包com.company.project.dbcom.company.projectcom.company.project.db.x 没有任何关系。与a.b.c 中的代码相比,com.company.project.db 中的代码无法访问com.company.project.db.x 中的代码。

【讨论】:

有趣,我从没想过它,但假设包级别的范围/可见性将适用于子包。所以 a.b.c 中的包级类对 a.b.c.d 是不可见的? 顺便说一句,Java 在文件结构中强制嵌套,不是吗?不是你的意思,但仍然很重要......java包不是你可以输入的任意文本(IIRC) @John:关于你的第一点:正确。关于第二点,java 工具通常会强制执行一个看起来像包命名的目录结构,是的,但同样,它只不过是一种约定,尽管它很强大。 @skaffman:我想我一定不同意。引用“Java TMVirtual Machine Specification”,2.7.5 完全限定名称:“作为另一个命名包的子包的命名包的完全限定名称由包含包的完全限定名称后跟“。”组成通过子包的简单(成员)名称。”所以 JVM 规范确实明确提到了包名层次结构。 [cont] 另一方面,JVM 说“每个 Java 虚拟机实现都决定了如何创建和存储包、编译单元和子包[...]”,因此放置类的规则将文件放入目录层次结构只是 Sun 的运行时实现碰巧使用的 ClassLoader 的约定。好的,足够的吹毛求疵:-)。【参考方案2】:

在 C++ 中,命名空间只是对可用名称进行分区。 Java 包是关于模块的。命名层次只是其中一个方面。

C++ 中深度嵌套的命名空间本身并没有什么问题,只是它们通常不是必需的,因为它们背后没有模块系统,而且额外的层只会增加噪音。 通常有一个或两个级别的命名空间就足够了,奇数的额外级别用于内部详细信息(通常称为详细信息)。

C++ 命名空间还有一些额外的规则,如果过度使用这些规则可能会发现你——例如argument-dependent-lookup,以及有关解析到父级别的规则。 WRT 后者,采取:

namespace a namespace b int x;  
namespace b string x; 
namespace a

  b::x = 42;

这合法吗?发生了什么很明显吗? 您需要知道命名空间解析的优先级才能回答这些问题。

【讨论】:

有趣。您能否添加一些有关“依赖查找”和“解析父级别”的链接?没听说过。 @sleske:完整的解释比您在评论中实际写的要长得多。基本思想是当编译器找到一个非完全限定标识符时,它将尝试从当前命名空间开始匹配它,如果没有找到,它将尝试封闭的命名空间......一直到根。这由 ADL(Argument Dependent Lookup)扩展,因此如果您在一组参数上调用函数,它也会尝试匹配参数命名空间中的方法标识符。 f( std::string(), ::ns::myclass() ) 还将在 stdns 命名空间中查找 f 感谢您的解释。至于 ADL:这似乎令人难以置信的复杂。我想知道这在语言设计中是否真的有必要。我更喜欢 Java 方法(不合格或完全合格)。好吧,C++ 从来没有因为过于简单而被原谅...... ADL 背后的最初想法是让您使用在第一个操作数所在的命名空间中已重载的运算符。当注意到这导致增强的接口原则(声明对同一名称空间内的其他对象“操作”的事物)时,它被扩展到任何功能。它有一定的优雅性,但同时又微妙且经常令人惊讶(并且曾经偶尔得到支持)【参考方案3】:

您可以在 C++ 中拥有嵌套的命名空间。

很明显,它们的工作方式与在 java 中不同。 Java 包的定义确实要好得多,并且没有真正的静态初始化怪异。使用 C++ 命名空间很有帮助,但仍涉及相当多的危险。

【讨论】:

请澄清你提到的这个“危险”。【参考方案4】:

在 C++ 中,设计和实现的基本单元是类,而不是命名空间。命名空间旨在防止大型库中的名称冲突,而不是用于表达概念。

类比命名空间有几个优点:

它们可以有构造函数和析构函数 他们可以有私人成员 它们无法重新打开

但是,我会仔细检查任何深度嵌套的关系。这确实不是设计软件的好方法,并且会导致代码不可读,无论您使用类还是命名空间。

【讨论】:

我认为您在比较错误的单位。包更像是 C++ 中的头文件,而不是类。 C 和 C++ 中模块化的基本单元是头文件和编译单元,而不是类。 “它们不能重新打开”——有些人会认为这是一个缺点;-) @Konrad - 在比较头文件和模块时我会小心。头文件在很大程度上是一个预处理器。如果说我们现在拥有的最密切对应的实体是翻译单元(cpp 文件)——尽管符号 通常通过头文件导出——但这不是必需的 @Phil 你不是建议 C++ 包含鸭子类型,我希望! @Neil - 你从哪里得到这个想法?【参考方案5】:

我认为之前的答案中缺少一些东西,这也是我非常喜欢 C++ 的原因之一。

想象你正在编写一个图形应用程序,突然你意识到你所有的小部件之间都有一些共同点。您希望它们都具有新功能。你是做什么的?

1) 编辑基础小部件类?好的,但很可能您无权访问它。也许有一个许可问题阻止您进行自己的修改。即使你能做到,如果它只对你的项目有意义,作者也不会在他们未来的版本中包含它,升级工具包会更痛苦

2) 创建接口类/多继承?根据您现有的代码,更新与小部件相关的每个类或多或少都会很痛苦。这样做,你的代码将花费更多的维护成本,因为每个定义新类的人都必须知道他们应该从你的接口继承。依赖别人的纪律真的很冒险。

C++ 命名空间的美妙之处在于,您可以通过一种额外的方式将stuff 封装在现有系统中。您不仅可以封装在无法编辑的现有库中,还可以封装无法轻松插入到类/对象层次结构中的类似概念。

Java 迫使您更多地关注纯 OOP 设计。当然我要告诉你的可能是一个肮脏的 hack 并且不优雅,但是有很多懒惰的人编程他们不花时间修复他们的设计。

【讨论】:

【参考方案6】:

以下是 C++ 命名空间与 Java 或 C# 命名空间不同以及如何不同的一些原因。

避免冲突

在 Java/C# 语言中,命名空间旨在避免类库不同部分中的名称发生冲突。您可能在 C# 命名空间层次结构的 5 个不同位置有名为“Watcher”的类。在 C++ 中,如果您的库中出现相同命名的类,那么您将放在另一个类中,而不是创建命名空间。这样的嵌套类都很好并且受到鼓励,实际上语法也将类视为使用 :: 运算符的命名空间。

嵌套命名空间应该有多少?

Boost、Eigen 等流行库,当然还有 STL 提供了很好的示例。这些库通常将几乎所有内容都填充到一个命名空间中,例如 std::boost::eigen::。很少有组件拥有自己的命名空间,例如 std::iosboost:filesystem。关于何时使用第二级没有一致的规则,但似乎很大或单独开发/维护或可选组件通常有自己的命名空间。第三层就更难得了。通常我使用结构company::project 和大型独立可用的项目子系统company::project::component

避免外部库中的冲突

现在有个大问题:如果您从两个不同的人那里获得两个具有完全相同名称空间和类的库怎么办?这种情况相当罕见,因为大多数人倾向于至少以项目名称包装他们的库。即使项目名称相同,您最终使用两者中的库的情况也更少。然而,有时会为项目名称做出错误的决定(ahm...“metro”、“apollo”...),甚至根本不使用名称空间。如果发生这种情况,请将其中一个或两个库的 #include 包装到命名空间中,冲突就解决了!这就是为什么人们不太关心冲突的原因之一,因为解决它们是微不足道的。如果您遵循使用company::project 的做法,那么冲突将变得非常罕见。

语言差异

尽管 C++ 像 C# 一样提供 using namespace 语句,但通常认为在您自己的命名空间中“导入”所有内容是一种不好的做法。这样做的原因是标题可能包含许多“坏”的东西,包括重新定义可能会让你完全惊讶的东西。这与 C#/Java 完全不同,在 C#/Java 中,当您执行相当于 using namespace 时,您才能获得干净的公共接口。 (旁注:在 C++ 中,您可以通过使用 Pimpl 模式来实现相同的目标,但它通常需要太多额外的管道,而且很少有库真正做到这一点)。所以你几乎不想做using namespace。相反,您为您实际想要使用的内容执行typedefs(或using name =)。这再次使深度嵌套的命名空间无法使用。

组织代码

在 Java/C# 中,人们倾向于在文件夹中组织代码。通常随着文件夹增长超过 20 甚至 10 个文件,人们会开始考虑文件夹。在 C++ 中,事情是more diverse,但对于许多大型项目,更扁平的目录结构是首选。例如,标准库的 std folder 有 53 个文件,而 Facebook 的 folly 项目似乎走同样的路线。我认为其中一个原因可能是 Java/C# 人员更多地使用可视化 IDE 并在文件夹导航中使用鼠标滚动,而不是在控制台中可以使用通配符在平面结构中查找文件。此外,C++ 程序员绝对不会回避将多个类放在单个文件中并将文件命名为逻辑单元,而不是与 C# 或 Java 中的类名相同。这使得编译速度更快,这对于大型项目非常重要。虽然对于每个文件夹都有自己的命名空间没有语言级别的要求,但许多 C++ 开发人员更喜欢为每个文件夹分配自己的命名空间,并将文件夹层次结构保持在 2 级或更少级别。

可能的异常

在 C++ 中,如果您已经在 A::B 中,则可以将 A::B::C::D 称为 C::D。因此,如果您有私有代码或较少使用的类想要进一步向下推,那么您可能会这样做,同时仍将您自己的相对深度保持在 2 左右。在这种情况下,您可能还想为每个级别创建文件夹,以便文件位置是可预测的。一般来说,这方面没有黄金标准,但您不希望过度使用模仿 C#/Java 的深度嵌套命名空间。

相关

Avoiding collisions in header files; use a namespace wrapper?

Is there a 'right' way to approach namespaces in C++

Using fully qualified names in C++

Java packages vs. C++ libraries

Is there a better way to express nested namespaces in C++ within the header

How do you properly use namespaces in C++?

【讨论】:

以上是关于C++ 命名空间,与 Java 包的比较的主要内容,如果未能解决你的问题,请参考以下文章

C++ 唯一类名与命名空间

C++问题:关于匿名命名空间

C++命名空间与缺省参数

C++命名空间与缺省参数

C++命名空间与缺省参数

c++之命名空间(namespace)