7

我知道 C++ 库应该使用命名空间来避免名称冲突,但是因为我已经必须:

  1. #include正确的标题(或转发声明我打算使用的类)
  2. 按名称使用这些类

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


例如,看一下 Xerces-C:它定义了一个Parser在命名空间内调用的纯虚拟接口XERCES_CPP_NAMESPACE。我可以Parser通过包含适当的头文件,然后导入命名空间using namespace XERCES_CPP_NAMESPACE或在声明/定义前面加上XERCES_CPP_NAMESPACE::.

随着代码的发展,可能需要放弃 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?

4

7 回答 7

10

在我看来,您的问题主要是由于您(ab)使用名称空间的方式,而不是名称空间本身。

  1. 听起来您将许多最小相关的“东西”投入到一个命名空间中,主要是(当您开始使用它时),因为它们恰好是由同一个人开发的。至少 IMO,命名空间应该反映代码的逻辑组织,而不仅仅是一堆实用程序碰巧由同一​​个人编写的意外。

  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,因此只要两个命名空间都实现了整个接口,我就可以使用其中任何一个,根本无需更改其余代码,并且使用对基类的指针/引用不会产生任何运行时开销具有用于实现的虚拟功能。

于 2010-02-04T20:07:08.260 回答
9

命名空间与耦合无关。无论你叫它useful::UsefulClass还是只是,都存在相同的耦合UsefulClass。现在,您需要进行所有重构工作这一事实只会告诉您代码在多大程度上依赖于您的库。

为了简化转发,您可以编写一个forward标头(在 STL 中有几个,您肯定可以在库中找到它),就像usefulfwd.h只转发定义库接口(或实现类或任何您需要的东西)一样。但这与耦合无关。

尽管如此,耦合和命名空间只是无关的。一朵玫瑰的任何其他名字都会闻起来一样甜美,并且您的类在任何其他名称空间中都是耦合的。

于 2010-02-04T19:58:39.260 回答
6

(a) 库中的接口/类/函数

不比你已经拥有的更多。使用namespace-ed 库组件可以帮助您避免命名空间污染。

(b) 命名空间推断的实现细节?

为什么?你应该包括的只是一个 header useful.h。实现应该是隐藏的(并且useful.cpp可能存在于动态库形式中)。

useful.h您可以通过声明选择性地仅包含您需要的那些类using useful::Useful

于 2010-02-04T19:43:29.717 回答
2

我想扩展 David Rodríguez 的第二段 - dribeas 的回答(赞成):

为了简化转发,您可以编写一个转发头(在 STL 中有几个,您肯定可以在库中找到它),例如仅转发定义库接口(或实现类或任何您需要的)的有用的fwd.h。但这与耦合无关。

我认为这指向了您问题的核心。命名空间在这里是一个红鲱鱼,你被低估了包含语法依赖关系的需要而被咬住了。

我能理解你的“懒惰”:过度设计(企业HelloWorld.java)不对的,但是如果你一开始就保持低调(这不一定是错误的)并且代码证明是成功的,那么成功就会拖累它高于它的联盟。诀窍是感知正确的时机,以切换到(或从需要出现的第一刻开始使用)一种以向前兼容的方式刮擦你的痒的技术。

对一个项目的闪闪发光的前向声明只是乞求第二轮及随后的轮次。您实际上不需要成为 C++ 程序员就可以阅读“不要前向声明标准流,<iosfwd>而是使用”的建议(尽管这已经是几年前的事了;1999 年?VC6 时代,肯定)。如果您稍作停顿,您会听到许多没有听从建议的程序员发出的痛苦尖叫。

可以理解保持低调的冲动,但你必须承认,这#include <usefulfwd.h>并不比class Useful, 和鳞片更痛苦。只需这个简单的委托就可以避免您N-1class Usefulto更改class useful::Useful

当然,它不会帮助您处理客户端代码中的所有用途。简单的帮助:事实上,如果您在大型应用程序中使用库,您应该将库提供的转发标头包装在特定于应用程序的标头中。这一点的重要性随着依赖的范围和库的波动性而增加。

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的问题。您可能不喜欢朗朗上口的 TLA,但它描述了一个真正的核心编程原则。

于 2010-02-04T22:13:34.677 回答
0

如果您有多个“有用”库的实现,那么它们是否同样可能(如果不在您的控制之下)使用相同的命名空间,无论是全局命名空间还是有用的命名空间?

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

任何一致的库演化策略都应该为 API 维护相同的命名空间。该实现可以利用对您隐藏的不同名称空间,并且这些名称空间可能会在不同的实现中发生变化。不确定这是否是“命名空间推断的实现细节”的意思。

于 2010-02-04T19:49:21.967 回答
0

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

消费者可以选择做

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

我的票总是最后一票 - 它是污染最少的

应该强烈劝阻第一个(行刑队)

于 2010-02-04T19:50:42.770 回答
0

好吧,事实是没有办法轻松避免 C++ 中的代码纠缠。但是,使用全局命名空间是最糟糕的想法,因为这样您就无法在实现之间进行选择。你以对象而不是对象结束。这在内部可以正常工作,因为您可以随时编辑源代码,但是如果有人将这样的代码发送给客户,他们不应该期望它们会持续很长时间。

一旦你使用了 using 语句,你也可以在全局中,但在 cpp 文件中使用它会很好。所以我会说你应该把所有东西都放在一个命名空间中,但是对于内部来说,它应该都是同一个命名空间。这样,其他人仍然可以使用您的代码而不会发生灾难。

于 2010-02-04T20:22:11.420 回答