使用C++命名空间会增加耦合吗?

7
我了解到C++库应该使用命名空间来避免名称冲突,但由于我已经:

  1. 正确包含头文件(或前向声明我想要使用的类)
  2. 按名称使用这些类

这两个参数不是已经暗示了命名空间所传达的相同信息吗?现在使用命名空间会引入第三个参数-完全限定名称。如果库的实现发生了变化,现在有三个潜在的事情需要改变。这难道不是根据定义增加了库代码和我的代码之间耦合度吗?


例如,看看Xerces-C:它在名称空间中定义了一个纯虚接口称为Parser。我可以通过包括适当的头文件并使用using namespace XERCES_CPP_NAMESPACE或prefacing declarations / definitions with XERCES_CPP_NAMESPACE :: 来在我的代码中使用Parser接口。

随着代码的演变,也许有必要放弃Xerces,采用不同的分析器。通过纯虚接口(如果我使用工厂来构建我的解析器,甚至更多),我部分地“受到”对库实现的更改的保护,但是一旦我从Xerces切换到其他内容,我需要检查所有的using namespace XERCES_CPP_NAMESPACE和XERCES_CPP_NAMESPACE :: Parser代码,然后进行修改。


最近我重构了一个现有的C++项目,将一些有用的功能分离出来成为库时,我遇到了这个问题:

foo.h

class Useful;  // Forward Declaration

class Foo
{
public:

    Foo(const Useful& u);
    ...snip...

}

foo.cpp

#include "foo.h"
#include "useful.h" // Useful Library

Foo::Foo(const Useful& u)
{
    ... snip ...
}

因为当时对此并不了解(部分原因是懒惰),所以整个useful.lib的功能都放在了全局命名空间中。

随着useful.lib的内容越来越多(并且越来越多的客户端开始使用这些功能),决定将useful.lib中的所有代码移到其自己的命名空间中,称为"useful"

客户端.cpp文件很容易修复,只需添加using namespace useful即可;

foo.cpp

#include "foo.h"
#include "useful.h" // Useful Library

using namespace useful;

Foo::Foo(const Useful& u)
{
    ... snip ...
}

但是.h文件真的很费力。为了避免在头文件中使用using namespace useful;而污染全局命名空间,我将现有的前向声明封装在命名空间中:

foo.h

namespace useful {
    class Useful;  // Forward Declaration
}

class Foo
{
public:

    Foo(const useful::Useful& u);
    ...snip...
}

有数十个(很多)文件,这最终成为了一个大问题!这应该不是那么困难的。显然,我在设计和/或实现方面做错了什么。

虽然我知道库代码 应该 在自己的命名空间中,但让库代码保留在全局命名空间中,并尝试管理 #includes 是否更有优势?


2
“(b)由命名空间推断出的实现细节” - 你能详细解释一下吗?我不明白这是什么意思。 - John Kugelman
6
在我看到问题之前,我差点睡着了,而且我现在仍然不太明白你在询问什么。你是否考虑过使用 "using useful::Useful;"? - anon
1
有很多文本。有没有一个简化版我可以阅读? - wheaties
3
@*: 请阅读倒数第二段。 - dirkgently
2
+1:不要理会那些喷子。我很喜欢阅读你的帖子! - John Dibling
显示剩余15条评论
7个回答

10

我感觉你的问题主要在于你如何(滥)使用命名空间,而不是命名空间本身。

  1. 看起来你把许多关系不大的“东西”都放进了一个命名空间中,主要是因为它们恰好被同一个人开发。我的看法是,命名空间应该反映代码的逻辑组织,而不仅仅是一堆工具恰好由同一个人编写。

  2. 命名空间名称通常应该相当长且具有描述性,以防止除最遥远的可能性之外的冲突。例如,我通常会包括我的姓名、编写日期和对命名空间功能的简短描述。

  3. 大多数客户端代码不需要(而且通常不应该)直接使用命名空间的实际名称。相反,它应该定义一个命名空间别名,并且在大多数代码中只使用别名。

将第二和第三点结合起来,我们可以得到如下代码:

#include "jdate.h"

namespace dt = Jerry_Coffin_Julian_Date_Dec_21_1999;

int main() {

    dt::Date date;

    std::cout << "Please enter a date: " << std::flush;
    std::cin>>date;

    dt::Julian jdate(date);
    std::cout   << date << " is " 
                << jdate << " days after " 
                << dt::Julian::base_date()
                << std::endl;
    return 0;
}
这个方法可以消除(或至少大大减少)客户端代码与日期/时间类实现之间的耦合。例如,如果我想重新实现相同的日期/时间类,我可以将它们放在不同的命名空间中,并通过更改别名并重新编译来切换它们。
实际上,我有时会将其用作一种编译时多态机制。举个例子,我编写了几个版本的小型“显示”类,一个在Windows列表框中显示输出,另一个通过iostreams显示输出。然后代码使用一个类似于别名的东西:
#ifdef WINDOWED
namespace display = Windowed_Display
#else
namespace display = Console_Display
#endif

剩下的代码只使用 display::whatever,所以只要两个命名空间都实现了整个接口,我就可以在不改变任何其他代码的情况下使用任意一个,并且不需要使用指向具有虚函数的基类的指针/引用来实现它们,也没有运行时开销。


1
作为一种运行时多态机制,应该是“编译时”吗? - Georg Fritzsche
@gf:哎呀,是的。谢谢。我会修复它的。 - Jerry Coffin
1
天啊,我绝对不会用我的姓和日期来命名我的命名空间...我们在工作中有几百个组件,它们的名称通常是组件的名称,以便区分它们 :) - Matthieu M.
@Matthieu: 关键是找出一个可遵循的模式,以确保其唯一性。我提倡的整个观点就是,你直接使用该名称的唯一时机几乎只有在它作为别名的目标时。 - Jerry Coffin
我理解,但是你的每个客户端都需要使用别名包装你的头文件或在每次包含时重新定义别名,这很麻烦。此外,作为命名约定,我使用命名空间作为文件路径 >> #include "name1/name2/class.h" 表示 name1::name2::Class,这使得很容易一眼看出类属于哪个模块(子模块...)以及哪个头文件引入了它。 - Matthieu M.

9
命名空间与耦合无关。不管你是用 useful::UsefulClass 还是只用 UsefulClass,耦合程度都是一样的。现在你需要进行所有这些重构工作的事实告诉你,你的代码在多大程度上依赖于你的库。
为了简化转发,你可以编写一个头文件 forward(STL 中有几个,你肯定可以在库中找到),比如 usefulfwd.h,它只转发定义了库接口(或者实现类或其他你需要的东西)。但这与耦合无关。
尽管如此,耦合和命名空间仍然没有关系。玫瑰花换了任何名字都一样香,你的类在任何其他命名空间中也是耦合的。

1
现在你需要做所有这些重构工作的事实告诉你,你的代码在多大程度上依赖于你的库。 这是一个非常好的观点。 我将代码打包成一个库供他人使用,但我的原始代码严重依赖于那个功能。 - Mike Willekes

6
(a) 来自库的接口/类/函数
使用命名空间(namespace)库组件可以帮助您避免命名空间污染,不需要更多的操作。
(b) 通过命名空间推断的实现细节?
为什么要这样做?您只需包含一个头文件useful.h即可。 实现应该是隐藏的(并且驻留在useful.cpp中,可能以动态库形式存在)。
您可以通过使用using useful :: Useful声明来选择性地仅包含所需的类从useful.h中。

2
我想扩展一下David Rodríguez - dribeas的答案中的第二段话(已获赞):
为了简化转发,您可以编写一个转发头文件(STL中有几个,您肯定可以在库中找到),例如usefulfwd.h,它只转发定义了库接口(或实现类或任何您需要的内容)。但这与耦合无关。
我认为这指向了您问题的核心。 命名空间在这里是一个红色的诱饵,您低估了包含语法依赖项的必要性。
我可以理解您的“懒惰”:过度工程化并不正确(企业HelloWorld.java),但如果您在开始时保持代码低调(这不一定是错误的),并且代码被证明成功,成功将使其超越自己的联盟。 诀窍是感觉切换到(或从第一刻起使用出现需要的技术)以前兼容的方法的正确时机。
在项目中闪亮的前向声明只是在请求第二和随后的回合。 您不需要成为C ++程序员就能读到建议“不要前向声明标准流,而是使用”(尽管这已经过去了几年; 1999年? VC6时代,绝对)。 如果您稍微停顿一下,您会听到许多程序员的痛苦尖叫声,他们没有听从建议。
我可以理解保持低调的冲动,但您必须承认,#include不比class Useful更痛苦,而且可以扩展。 只需这个简单的委托即可使您免除从class Useful到class useful :: Useful的N-1次更改。
当然,它无法帮助您处理客户端代码中的所有用途。 简单的帮助:实际上,如果在大型应用程序中使用库,则应将库提供的前向头文件包装在特定于应用程序的头文件中。 这种依赖性的范围和库的易变性会增加其重要性。
src / libuseful / usefulfwd.h
#ifndef GUARD
#define GUARD
namespace useful {
    class Useful;
} // namespace useful
#endif

src/myapp/myapp-usefulfwd.h

#ifndef GUARD
#define GUARD
#include <usefulfwd.h>
using useful::Useful;
#endif

基本上,它是保持代码DRY的问题。你可能不喜欢这个时髦的缩略语,但它描述了一个真正的核心编程原则。


0

不,你并没有增加耦合。正如其他人所说 - 我不明白命名空间的使用如何泄漏实现

消费者可以选择做什么

 using useful;
 using useful::Foo;
 useful::Foo = new useful::Foo();

我的选择总是最后一个 - 它是最少污染的

第一个应该受到强烈谴责(通过行刑队)


你大概是想写成 using namespace useful; 吧? - Jerry Coffin

0

如果你的“有用”库有多个实现,那么它们使用相同的命名空间的可能性是相等的(如果不是由你控制的话),不管是全局命名空间还是“有用”命名空间。

换句话说,使用命名空间与使用全局命名空间与你对一个库/实现的“耦合程度”无关。

任何一个连贯的库演进策略都应该保持 API 使用相同的命名空间。实现可以使用不同的命名空间,这些命名空间对你来说是隐藏的,并且在不同的实现中可能会发生变化。不确定这是否是你所指的“由命名空间推断出的实现细节”。


0

实际上,在C++中很难避免代码的纠缠。使用全局命名空间是最糟糕的想法,因为你没有办法在实现之间进行选择。你最终会得到Object而不是Object。这在内部工作时可以正常工作,因为你可以随时编辑源代码,但如果有人将这样的代码发送给客户,他们不应该期望它们长久存在。

一旦你使用了using语句,你就可以像在全局命名空间中一样使用它,但在cpp文件中使用它可能会更好。所以我建议你应该把所有东西都放在一个命名空间中,但对于内部使用,它应该都是相同的命名空间。这样其他人仍然可以使用你的代码而不会造成灾难。


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