每个类都应该有自己的命名空间吗?

12

有一件困扰我一段时间的事情:

目前的智慧是,类型应该被保留在一个命名空间中,该命名空间只包含作为类型非成员接口一部分的函数(请参见《C++ 编码规范》Sutter 和 Alexandrescu,或此处),以防止 ADL 引入无关定义。

这是否意味着所有类都必须拥有自己的命名空间?如果我们假设一个类可能会在未来通过添加非成员函数来进行增强,那么将两个类型放入同一个命名空间就永远不安全了,因为其中任何一个都可能引入可能与另一个干扰的非成员函数。

我问这个问题的原因是,命名空间对我来说变得很麻烦。我正在编写一个头文件库,我发现自己正在使用诸如project::component::class_name::class_name之类的类名。它们的实现调用帮助函数,但由于这些函数不能在同一个命名空间中,它们也必须被完全限定!

编辑:

有几个答案建议 C++ 命名空间只是避免名称冲突的机制。这并不正确。在 C++ 中,采用参数的函数会使用参数依赖查找进行解析。这意味着当编译器尝试查找与函数名匹配的函数定义时,它将查看与其参数类型的命名空间中的每个函数来查找候选项。

这可能会带来意外的、不愉快的后果,详见A Modest Proposal: Fixing ADL。Sutter和Alexandrescu的规则是:除非函数属于该类的接口,否则永远不要将函数放在同一命名空间中。我不知道如何遵守这个规则,除非准备给每个类分配自己的命名空间。

欢迎提出更多建议!


2
他们的实现调用必须完全限定的辅助函数 - 如果有帮助的话,可以在函数体内放置一个using声明或指令。 - Steve Jessop
4
拜托,请不要强制要求类和命名空间之间保持1:1的比例。这违背了使用命名空间的初衷,还会增加很多额外的工作量。 - user2189331
3
@James D:我不同意。 我认为这还不够。我们还需要确保每个命名空间本身也在命名空间中。 - MaxGuernseyIII
1
告诉我,把 void foo(int)class bar; 放在同一命名空间中到底有什么问题?自变量查找(ADL)会导致什么问题? - jalf
1
如果我正确理解了Sutter的话,当将bar传递给函数void baz(bar b)时,baz的实现会从bar的命名空间中全部调用函数。如果baz恰好调用了一个叫做foo的东西,并且void foo(int)比它要找的那个更匹配,那么它就会选择错误的foo - thehouse
5个回答

16

不,我从未听说过这种惯例。通常每个库都有自己的命名空间,如果该库有多个不同的模块(例如,不同的逻辑单元在功能上不同),那么这些模块可能会有自己的命名空间,尽管一个库每个命名空间就足够了。在库或模块命名空间内,您可以使用命名空间detail或匿名命名空间来存储实现细节。每个类使用一个命名空间是完全过度的,在我的看法中,我肯定会避免这样做。同时,我强烈建议您为您的库至少拥有一个命名空间,并将所有内容放在该命名空间或其子命名空间中,以避免与其他库发生名称冲突。

为了让这更具体化,让我举一个古老的Boost C++库作为例子。所有boost中的元素都驻留在boost::中。有一些模块在Boost中,例如进程间库,它有自己的命名空间,例如boost::interprocess::,但就大部分而言,boost的元素(特别是那些经常跨模块高频使用的元素)仅驻留在boost::中。如果您查看boost,它经常使用boost::detailboost::name_of_module::detail来存储给定命名空间的实现细节。我建议您以这种方式对命名空间进行建模。


+1 表示“不是”,然而我不会为每个库添加一个。一个库是一个实现单元。命名空间通常与团队、项目、子项目有关。您可以拥有跨越两个库和一个可执行文件的命名空间。 - pm100
1
@pm100,如果您有一个多模块项目(具有多个库或其他逻辑单元),那么您可以将它们统一在一个命名空间下,但我仍建议每个库使用一个命名空间进行内部细分。 - Michael Aaron Safyan

9
不,不,再三强调不!在C++中,命名空间不是架构或设计元素。它们只是一种防止名称冲突的机制。如果实际上你没有名称冲突,那么你就不需要使用命名空间。

4
“它们只是一种防止名称冲突的机制。”这是错误的。命名空间也用于指导 ADL。不管怎样,它们的“作用”远不如它们实际上所做的事情重要,如果它们在某些特定情况下的作用是不可取的。 (注:ADL 指 Argument Dependent Lookup, 即关联参数查找) - Steve Jessop
2
命名空间并不仅仅是一个简单的命名机制!通过参数依赖查找(http://en.wikipedia.org/wiki/Argument_dependent_name_lookup),在与类相同的命名空间中的每个函数都会被视为候选项,当类的对象被使用时。因此 Sutter 和 Alexandrescu 提出了这条规则。 - thehouse
2
@steve @thehouse 命名空间是一种简单的冲突避免机制 - 如果您以这种方式设计软件的话。 - anon
2
@Neil:是的,如果他们在不完全限定它的情况下调用该函数,并传递一个模板参数类型的对象作为参数,而该类型恰好是命名空间中包含同名但无关的函数的UDT,则可能无法正常工作。因此,编写这些模板函数的人可能会低估其模板参数类型的要求。如果您正在编写库和可移植代码并希望正确记录它们,那么没有忽略此问题并表现得好像它不会影响您的选项。 - Steve Jessop
2
嗯,我写的代码可能会被我从未见过的人调用,所以这是我考虑的问题。许多用户意味着许多命名空间。实际上,我从未发布过C++模板库,这就是为什么我处于“是的,我看到了文档化这个问题会有问题”的阶段,而不是“啊哈,我以前做过所有这些并且有答案。这是我使用的规则…”或“是的,我们注定要失败,ADL正如Sutter所说的那样出了问题”。 - Steve Jessop
显示剩余32条评论

7
为了避免ADL,你只需要两个命名空间:一个包含所有类,另一个包含所有松散函数。ADL绝对不是每个类都应该有自己的命名空间的好理由。
现在,如果你希望通过ADL找到某些函数,你可能需要为此目的创建一个命名空间。但是,实际上很少需要为每个类单独创建一个命名空间来避免ADL冲突。

0

这是一篇非常有趣的论文,但考虑到作者,这也是有很大可能性的。然而,我注意到问题主要涉及:

  • typedef,因为它们只引入别名而不是新类型
  • 模板

如果我执行:

namespace foo
{
  class Bar;

  void copy(const Bar&, Bar&, std::string);
}

然后调用它:

#include <algorithms>

#include "foo/bar.h"

int main(int argc, char* argv[])
{
  Bar source; Bar dest;
  std::string parameter;
  copy(source, dest, parameter);
}

然后它应该选择 foo::copy。实际上,它将考虑 foo::copystd::copy 但是由于 foo::copy 不是模板,因此会被优先考虑。


如果泛型函数比非泛型函数更匹配,编译器会毫不犹豫地选择泛型函数。在您的情况下,您是可以的,但并不是因为 foo::copy 不是模板,而是因为允许使用 std::copy 会导致调用不明确。如果 parameter 是一个字符串字面量,那么将选择 std::copy,因为它可以直接接受这样的值,而 foo::copy 则需要进行转换。 - Dennis Zickefoose
实际上,通过将“parameter”设置为字符串字面量,std::copy 将不再被考虑,所以你仍然没问题。但是一般的观点仍然成立。 - Dennis Zickefoose

0

可能不是。请参考Eric Lippert在这个主题上的帖子

以下几点需要注意:

  1. Eric Lippert是C#设计师,但他所说的有关错误的分层设计也适用于此。
  2. 该文章中描述的许多内容都与在C#中将类的名称命名为命名空间相同,但同样的缺陷也适用于C++。

通过使用typedef可以减少一些类型定义的痛苦,但这当然只是一个权宜之计。


1
Eric Lippert的建议对于C#,甚至整个.Net来说都很好,但是它并不适用于C++,因为C++命名空间的工作方式不同。在这种情况下,需要使用C++命名空间来避免非类函数产生歧义。 - Gabe
你仍然不应该让类名和命名空间名称相同。你仍然会遇到相同类型的解析问题。我同意非类函数是C#不必处理的一个小问题,但是C#也有C++没有的扩展方法,所以在这方面它们差不多。无论如何,我同意你在这里有更好的答案。但我不讨厌自己的回答到要删除它。 - Billy ONeal

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