C++中头文件中有更好的表达嵌套命名空间的方法吗?

130

我从C++转换到Java和C#,认为这些语言中命名空间/包的使用更好(结构化程度更高)。然后我回到了C++,试图以相同的方式使用命名空间,但在头文件中所需的语法令人不堪入目。

namespace MyCompany
{
    namespace MyModule
    {
        namespace MyModulePart //e.g. Input
        {
            namespace MySubModulePart
            {
                namespace ...
                {
                    public class MyClass    

以下内容对我来说也很奇怪(为了避免深度缩进):

namespace MyCompany
{
namespace MyModule
{
namespace MyModulePart //e.g. Input
{
namespace MySubModulePart
{
namespace ...
{
     public class MyClass
     {

有没有更简洁的方式来表达上述内容?我是否缺少像这样的东西

namespace MyCompany::MyModule::MyModulePart::...
{
   public class MyClass

更新

好的,有人说Java/C#和C++中的命名空间概念是不同的。真的吗?我认为(动态)类加载不是命名空间的唯一目的(这是一个非常专业的理性观点)。为什么不能用它来提高可读性和结构化程度,比如考虑 "IntelliSense"。

目前,命名空间与其中的内容之间没有逻辑 / 粘合剂。Java 和 C# 做得更好... 为什么要包含 <iostream> 并使用命名空间 std? 如果你说逻辑应该依赖于头文件的包含,那为什么 #include 不使用类似于 "IntelliSense" 友好的语法,比如 #include <std::io::stream> 或者 <std/io/stream>?我认为缺少默认库的结构化是 C++ 相对于 Java/C# 的一个弱点。

如果避免冲突的独特性是一个点(这也是 C# 和 Java 的一个点),那么使用项目名称或公司名称作为命名空间是一个好主意,你不觉得吗?

一方面,有人说 C++ 是最灵活的...但每个人都说 "不要这样做"? 对我来说,C++ 可以做很多事情,但在许多情况下,即使是最简单的事情,它的语法也很可怕,相比之下 C# 更好。

更新2

大多数用户说创建超过两个级别的深度嵌套是无意义的。那么 Win8 开发中的 Windows::UI::Xaml 和 Windows::UI::Xaml::Controls::Primitives 命名空间怎么办?我认为微软使用命名空间是有道理的,而且确实比只有2个级别更深。 我认为更大的库 / 项目需要更深的嵌套(我讨厌类名像 ExtraLongClassNameBecauseEveryThingIsInTheSameNameSpace...然后你也可以把一切都放到全局命名空间中。)

更新3-结论

大多数人说 "不要这样做",但是...甚至 boost 的嵌套层次也比一个或两个层次深。是的,它是一个库,但是:如果你想要可重用的代码-把自己的代码当作你给别人的库。我还使用更深的命名空间嵌套来进行发现。


3
namespace е…ій”®еӯ—зҡ„дҪҝз”ЁжҳҜеҗҰжңүж»Ҙз”Ёпјҹ - Nawaz
4
命名空间和C#/Java模块系统的目的不同,因此您不应尝试以相同的方式使用它们。没有更简单的语法,因为为本不应该做的事情提供语法来简化操作是没有意义的。 - PlasmaHH
3
@PlasmaHH 提到了针对/在包含头文件后的Intellisense和其他辅助工具。一个公司内的大型项目可能需要多个嵌套(例如vw::golflib::io),以清晰地说明命名空间包含哪些“范围”。当然,您可以只使用vw::,但是如果命名空间旨在用于避免冲突,为什么声明起来如此麻烦?这导致没有人使用它或者只使用深度为一的命名空间(如经常建议的那样)。 - Beachwalker
@Stegi:智能感知是一个无意义的争论,如果它无法处理通过头文件名称导入的问题,那么你应该修复它,而不是改变语言使其更好地工作。我的自动完成功能完全正常,没有无休止的嵌套命名空间。嵌套命名空间会打开指数级的命名空间。如果您需要将命名空间嵌套5层深,因为您有32个具有相同名称的类,则您正在做一些非常错误的事情。命名空间不能替代文档中的分组。 - PlasmaHH
1
谢谢您提出这个问题,我也觉得C++确实缺少这样的功能! - alexpanter
显示剩余3条评论
11个回答

181

7
通过编译器开关 /std:c++latest 实现。 - sunny moon
4
请注意,如果您在 Visual Studio 2015 中使用 /std:c++latest 并且同时使用 Boost,当您包含某些 Boost 头文件时,可能会遇到非常神秘的编译器错误。我遇到了这个问题,就像这个 StackOverflow 问题中所描述的那样。 - Software Craftsman
1
它可以直接使用,命名空间为A::B::C。我已经使用g++-6.0进行了测试。 - ervinbosenbacher
虽然匿名/未命名命名空间不被接受(标准中未进行检查,但所有主要编译器套件都会拒绝它),但在这里我们仍然依赖于单独的命名空间声明,但至少周围的命名空间可以被压缩 ;) - Aconcagua

33
为了避免过深的缩进,我通常会这样做:
namespace A { namespace B { namespace C
{
    class X
    {
        // ...
    };
}}}

3
请注意,clang-format无法将您展示的代码进行格式化。有关更多信息,请参阅http://clang.llvm.org/docs/ClangFormatStyleOptions.html(NamespaceIndentation)。 - KindDragon

17

我完全支持peterchen的回答,但我想补充一些内容,以解决你问题的另一个方面。

在C++中声明命名空间是非常罕见的情况之一,我实际上喜欢使用#define

#define MY_COMPANY_BEGIN  namespace MyCompany { // begin of the MyCompany namespace
#define MY_COMPANY_END    }                     // end of the MyCompany namespace
#define MY_LIBRARY_BEGIN  namespace MyLibrary { // begin of the MyLibrary namespace
#define MY_LIBRARY_END    }                     // end of the MyLibrary namespace

这也消除了在命名空间结束大括号附近添加注释的需要(您是否曾经滚动到大型源文件的底部,并尝试添加/删除/平衡缺少关于哪个大括号关闭哪个作用域的注释?不好玩)。
MY_COMPANY_BEGIN
MY_LIBRARY_BEGIN

class X { };

class Y { };

MY_LIBRARY_END
MY_COMPANY_END

如果您想将所有命名空间声明放在一行上,您也可以使用一些(相当丑陋的)预处理器技巧来实现:
// helper macros for variadic macro overloading
#define VA_HELPER_EXPAND(_X)                    _X  // workaround for Visual Studio
#define VA_COUNT_HELPER(_1, _2, _3, _4, _5, _6, _Count, ...) _Count
#define VA_COUNT(...)                           VA_HELPER_EXPAND(VA_COUNT_HELPER(__VA_ARGS__, 6, 5, 4, 3, 2, 1))
#define VA_SELECT_CAT(_Name, _Count, ...)       VA_HELPER_EXPAND(_Name##_Count(__VA_ARGS__))
#define VA_SELECT_HELPER(_Name, _Count, ...)    VA_SELECT_CAT(_Name, _Count, __VA_ARGS__)
#define VA_SELECT(_Name, ...)                   VA_SELECT_HELPER(_Name, VA_COUNT(__VA_ARGS__), __VA_ARGS__)

// overloads for NAMESPACE_BEGIN
#define NAMESPACE_BEGIN_HELPER1(_Ns1)             namespace _Ns1 {
#define NAMESPACE_BEGIN_HELPER2(_Ns1, _Ns2)       namespace _Ns1 { NAMESPACE_BEGIN_HELPER1(_Ns2)
#define NAMESPACE_BEGIN_HELPER3(_Ns1, _Ns2, _Ns3) namespace _Ns1 { NAMESPACE_BEGIN_HELPER2(_Ns2, _Ns3)

// overloads for NAMESPACE_END
#define NAMESPACE_END_HELPER1(_Ns1)               }
#define NAMESPACE_END_HELPER2(_Ns1, _Ns2)         } NAMESPACE_END_HELPER1(_Ns2)
#define NAMESPACE_END_HELPER3(_Ns1, _Ns2, _Ns3)   } NAMESPACE_END_HELPER2(_Ns2, _Ns3)

// final macros
#define NAMESPACE_BEGIN(_Namespace, ...)    VA_SELECT(NAMESPACE_BEGIN_HELPER, _Namespace, __VA_ARGS__)
#define NAMESPACE_END(_Namespace, ...)      VA_SELECT(NAMESPACE_END_HELPER,   _Namespace, __VA_ARGS__)

现在你可以这样做:
NAMESPACE_BEGIN(Foo, Bar, Baz)

class X { };

NAMESPACE_END(Baz, Bar, Foo) // order doesn't matter, NAMESPACE_END(a, b, c) would work equally well

Foo::Bar::Baz::X x;

如果嵌套的层数超过三层,您需要添加帮助宏来达到所需的数量。


2
尽管我不喜欢#define,但我对预处理器的魔法印象深刻...只是如果我不必为更深层次的嵌套添加额外的辅助宏...好吧,反正我也不会使用它... - galdin

13

C++ 命名空间被用来分组接口,而不是用来划分组件或表达政治分歧。

标准规定禁止类似 Java 的命名空间使用方式。例如,命名空间别名 提供了一种方便使用深度嵌套或较长命名空间名称的方法。

namespace a {
namespace b {
namespace c {}
}
}

namespace nsc = a::b::c;

但是namespace nsc {}将会是一个错误,因为命名空间只能使用其原始命名空间名称进行定义。实质上,标准使得库的用户容易使用但让实现者变得困难。这阻止了人们编写这样的东西,但如果他们这么做了,则可以缓解影响。

应该每个接口都有一个命名空间,由相关类和函数定义。内部或可选的子接口可以放在嵌套命名空间中。但超过两层深度应该是非常严重的警示信号。

考虑在不需要:: 运算符的情况下使用下划线字符和标识符前缀。


20
好的,那么关于Win8开发中的Windows::UI::Xaml和Windows::UI::Xaml::Controls::Primitives名称空间,我认为微软对名称空间的使用是有意义的,确实比只有两个级别更深入。 - Beachwalker
4
使用不到2个级别的名称空间是一个警示信号,使用3或4个级别则完全没有问题。试图实现扁平的命名空间层次结构,当它没有意义时,会挫败命名空间的主要目的——避免名称冲突。我同意你应该为接口使用一个级别,另一个级别用于子接口和内部接口。但是,在此基础上,对于小到中型公司,您需要至少一个公司命名空间级别,或者对于大型公司,需要两个级别,即公司和部门级别。否则,您的接口命名空间将与其他开发名称相同的接口产生冲突。 - Kaiserludi
@Kaiserludi company::division 相对于 company_division 有哪些技术优势? - Potatoswatter
1
在公司内部的另一个部门,您可以只使用更短的“division”来引用company::division,即使在标题中也应该强烈避免使用“using namespace”污染更高级别的命名空间。 在公司命名空间之外,当分区名称与范围内的任何其他命名空间不冲突时,仍然可以执行“using namespace company;”,但是当某些分区命名空间内的接口名称发生冲突时,您无法执行“using namespace company_division;”。 - Kaiserludi
@Kaiserludi 换句话说,这并不涉及 ADL 或其他查找方面的任何优势,而是为了在引用内部命名空间时节省 company 中的按键次数。此外,这种输入/简洁性问题已经通过命名空间别名得到解决。 - Potatoswatter
4
重点是你几乎免费获取它(company::division和company_division的长度相同),而且不需要先定义一个额外的命名空间别名才能使用它。 - Kaiserludi

7
不,而且请不要这样做。命名空间的主要目的是解决全局命名空间中的冲突。其次是本地符号缩写;例如,复杂的UpdateUI方法可以使用using namespace WndUI来使用更短的符号。
我在一个1.3MLoc项目中,我们只有以下命名空间: - 导入的外部COM库(主要用于隔离#import#include windows.h头文件之间的冲突); - 一级“公共API”命名空间,用于某些方面(UI、数据库访问等); - “实现细节”命名空间,不属于公共API(在.cpp中的匿名命名空间中或仅为头文件库使用的ModuleDetailHereBeTygers命名空间); - 枚举类型是我经验中最大的问题,它们会导致污染。 我仍然觉得这是完全多余的命名空间。
在这个项目中,类名等使用两个或三个字母的“区域”代码(例如,CDBNode 而不是 DB::CNode)。如果您喜欢后者,可以有第二层“公共”命名空间,但不要再多了。
特定于类的枚举等可以是这些类的成员(尽管我认为这并不总是好的,有时很难说你是否应该这样做)。
除非您遇到了分发为二进制文件、没有提供自己的命名空间并且很难将其放入一个命名空间中的第三方库(例如,在二进制分发中),否则很少需要“公司”命名空间。尽管如此,在我的经验中,强制将它们放入一个命名空间中要容易得多。

[编辑] 根据Stegi的跟进问题:

好的,那么Win8开发中的Windows::UI::Xaml和Windows::UI::Xaml::Controls::Primitives命名空间呢?我认为Microsoft对命名空间的使用是有意义的,它确实比仅有2个级别更深

如果我没有表达清楚,我很抱歉:两层并不是一个硬性限制,而且更多也不是本质上的坏事。从我的经验来看,即使在大型代码库中,你很少需要超过两个级别。嵌套更深或更浅是一种权衡。

现在,微软的情况可能是不同的。假定有一个更大的团队,而且所有的代码都是库。

我认为微软这里是在模仿.NET库的成功,其中命名空间有助于广泛库的发现性。(.NET有约18000种类型。)

我进一步假设在命名空间中有一个最佳符号数量的数量级。例如,1没有意义,100听起来合适,10000显然太多了。
TL;DR:这是一个权衡,我们没有硬性数字。保持安全,不要在任何方向上过度。 “不要那样做”只是来自于“你有问题,我也会有问题,而且我不认为你需要它”的想法。

11
Win8开发中的Windows::UI::Xaml和Windows::UI::Xaml::Controls::Primitives名称空间怎么样?我认为微软对名称空间的使用很有意义,而且确实比只有两级深。 - Beachwalker
2
如果我需要全局访问的常量,我喜欢将它们放在一个名为“Constants”的命名空间中,然后创建适当命名的嵌套命名空间来对常量进行分类;如有必要,我会使用更多的命名空间来防止名称冲突。这个“Constants”命名空间本身包含在程序系统代码的通用命名空间中,例如名为“SysData”。这样就创建了一个包含三个或四个命名空间的完全限定名称(例如“SysData::Constants::ErrorMessages”,“SysData::Constants::Ailments::Bitflags”或“SysData::Defaults::Engine::TextSystem”)。 - Justin Time - Reinstate Monica
1
当实际代码需要常量时,任何需要它们的函数都使用using指令引入适当的名称,从而最小化冲突名称的可能性。我发现这可以提高可读性,并有助于记录给定代码块的依赖关系。除了常量之外,如果可能的话,我倾向于将其保留为两个命名空间(例如SysData::ExceptionsSysData::Classes)。 - Justin Time - Reinstate Monica
2
总的来说,在一般情况下,最好使用最少数量的嵌套命名空间,但如果出于某些原因需要全局对象(无论是常量还是可变的,最好是前者),则应使用多个嵌套命名空间将它们分成适当的类别,以记录其用法并最小化潜在的名称冲突。 - Justin Time - Reinstate Monica
3
如果没有客观的原因,只是说“请不要这样做”(尽管后来进行了澄清),我会给这个回答点一个负分。这种语言支持嵌套命名空间,项目可能有使用它们的很好理由。如果讨论可能存在的这些理由以及任何具体、客观的缺点,那么我的负分将被撤销。 - TypeIA
“在我的经验中,枚举类型是最大的问题。它们会像疯子一样污染代码。” 好吧,只需将每个枚举类型放在自己的命名空间中,并且永远不要在枚举类型的命名空间上使用“using”,而是通过“EnumNamespaceName :: enumValue”访问枚举值。这样它们就不会污染您的项目命名空间了。 - Kaiserludi

5

这里是来自 Lzz(懒人C++)文档的一句话:

Lzz recognizes the following C++ constructs:

namespace definition

An unnamed namespace and all enclosed declarations are output to the source file. This rule overrides all others.

The name of a named namespace may be qualified.

   namespace A::B { typedef int I; }

is equivalent to:

   namespace A { namespace B { typedef int I; } }

当然,这些工具所依赖的源代码的质量有争议...我认为这更像是一种好奇心,表明C++引发的语法问题可以采用很多形式(我也有自己的问题...)


3
这篇文章涵盖了该主题:命名空间文档 基本上,命名空间越长,人们使用using namespace指令的可能性就越大。
因此,在下面的代码示例中,您可以看到这会对您造成伤害:
namespace abc { namespace testing {
    class myClass {};
}}

namespace def { namespace testing {
    class defClass { };
}}

using namespace abc;
//using namespace def;

int main(int, char**) {
    testing::myClass classInit{};
}

这段代码可以编译通过,但是如果你取消注释//using namespace def;这一行,那么"testing"命名空间将变得模糊不清,并且会发生命名冲突。这意味着通过包含第三方库,你的代码库可能从稳定变为不稳定。

在C#中,即使您使用using abc;using def;,编译器也能够识别testing::myClass甚至只是myClass仅存在于abc::testing命名空间中,但是C ++不会识别它并被检测为冲突。


2

两个标准(C++2003和C++11)都非常明确,命名空间的名称是一个标识符。这意味着需要显式嵌套头文件。

我的印象是,允许在命名空间的简单名称旁边放置限定符标识符并不是什么大问题,但出于某种原因,这是不允许的。


0

[编辑:]
自从c++17,嵌套命名空间被支持为标准语言特性(https://en.wikipedia.org/wiki/C%2B%2B17)。目前为止,g++8还不支持这个特性,但是可以在clang++6.0编译器中找到。


[结论:]
使用clang++6.0 -std=c++17作为默认的编译命令。然后一切都应该正常工作-你将能够在你的文件中编译namespace OuterNS::InnerNS1::InnerNS2 { ... }


[原始答案:]
由于这个问题有点老了,我假设你已经解决了。但对于其他人,仍在寻找答案的人,我想出了以下想法:

Emacs buffers showing main file, namespace files, compilation command/result, and command-line execution.

(我可以在这里宣传Emacs吗 :) ?) 发布图片比仅仅发布代码更容易,也更易读。我不打算提供所有边角情况的完整答案,我只是想给一些启示。 (我完全支持C#,并认为在许多情况下,C ++应该采用一些面向对象的特性,因为C#主要因其相对易用而受欢迎)。

更新:由于C++17具有嵌套命名空间(http://www.nuonsoft.com/blog/2017/08/01/c17-nested-namespaces/),因此我的答案似乎已不再相关,除非您正在使用旧版本的C ++。 - alexpanter
3
尽管我非常喜欢Emacs,但发布图片并不是最理想的方式。这样会逃避文本搜索/索引,并且也会让视力受损的访客难以访问你的答案。 - Heinrich supports Monica

0

是的,你必须这样做

namespace A{ 
namespace B{
namespace C{} 
} 
}

然而,你正在试图以一种它们本不应该使用的方式使用命名空间。查看this这个问题,也许你会发现它有用。

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