为什么我们不能在类内部声明命名空间?

77
在类内部声明一个类是有效的。(嵌套类)
在类内部声明一个命名空间是无效的。
问题是:除了C++的语法问题之外,是否有任何好的理由禁止在类内部声明命名空间?
关于为什么我想要这样做,这里有一个例子:
让我们来看一个二叉树容器的基本声明。
template<typename Data>
class binary_tree
{
 public:
  ... stuff ....     

 private:
  ... iterators class declaration ...

 public:
  typedef left_depth_iterator_impl     left_depth_iterator;
  typedef right_depth_iterator_impl    right_depth_iterator;
  typedef left_breadth_iterator_impl   left_breadth_iterator;
  typedef right_breadth_iterator_impl  right_breadth_iterator;

  ... stuff ....     

 private:
  Data         data;
  binary_tree* left;
  binary_tree* right;
};

现在我注意到我的类中有很多迭代器,所以我想将它们重新分组到同一个命名空间中,就像这样:

template<typename Data>
class binary_tree
{
 public:
  ... stuff ....     

 private:
  ... iterators class declaration ...

 public:
  namespace iterator
  {
    typedef left_depth_iterator_impl     left_depth;
    typedef right_depth_iterator_impl    right_depth;
    typedef left_breadth_iterator_impl   left_breadth;
    typedef right_breadth_iterator_impl  right_breadth;
  }

  ... stuff ....     

 private:
  Data         data;
  binary_tree* left;
  binary_tree* right;
};

这将允许简单使用:
void  function()
{
  binary_tree::iterator::left_depth   it;

  ...stuff...
}

这在我使用类而不是命名空间时有效,但我被迫声明一个永远不会被实例化的类,这实际上是一个命名空间。
为什么允许在类中嵌套类,却禁止在类中嵌套命名空间?这是一个遗留的负担吗?

希望能给出有语义理由的答案,不仅仅引用标准的一部分(尤其是语法部分)。谢谢 :)


20
特性默认情况下未实现。缺少某个特性并不需要证明其合理性。相反,所有的特性都必须通过展示它们的优点超过了它们的成本来证明其价值。作为提出特性的人,你有责任描述为什么认为该特性有价值;而不是我来解释为什么没有该特性。Eric Lippert在https://dev59.com/7V7Va4cB1Zd3GeqPMr5s上的回答中有相关内容。 - Robert Cooper
罗伯特提供的答案非常好,我希望我能给它点赞多几次。 - WhozCraig
你可以这样做... #define inclass_namespace struct.. 然后你就可以像这样使用它... - joy
1
在阅读有关Smalltalk及其方法协议的文章后,我和@drax有着同样的问题。这些协议是通过将方法按其目的分组来使用的。请参见The Art and Science of Smalltalk,第66页,第“标准协议”章节。协议的目的是帮助编写类和使用它的程序员。 - Marek Niepiekło
我认为@RobertCooper所说的可能是正确的,但也有很大的可能性是某个功能确实被提出了,但由于某些原因被拒绝了。或者已经存在更好的替代方案。 - apple apple
6个回答

66

由于您询问了标准规定名称空间位置的哪些部分,我们首先看一下:

C++11 7.3-p4: 每个名称空间定义应出现在全局作用域或名称空间作用域中(3.3.6)。

关于类定义和在其中声明名称空间的提议,我带给你...

C++11 9.2-p2: 类被视为完整定义的对象类型(3.9)(或完整类型),位于类说明符的}处。在类成员说明中,在函数体、默认参数、异常说明以及非静态数据成员的括号或等号初始化程序中(包括嵌套类中的这些内容)中,类被视为在函数体内是完整的。否则,它将被视为在其自己的类成员说明中不完整。

因此,一旦到达右花括号,类定义就是有限的。它不能被重新打开和扩展(派生是另一回事,但不是扩展刚刚定义的类)。

但是,在名称空间的标准定义的最开始处潜伏着扩展它的能力;为了缺乏更好的术语而扩展它:

C++ 7.3-p1: 名称空间是一个可选命名的声明性区域。名称空间的名称可用于访问在该名称空间中声明的实体;也就是说,名称空间的成员。 与其他声明区域不同,名称空间的定义可以分布在一个或多个翻译单元的若干部分中。(强调添加)。

因此,在类内部的名称空间将违反7.3-p4的定义。假设没有这个限制,任何地方都可以(包括类内)声明名称空间,但是由于类的定义一旦关闭就被正式化,如果您遵守7.3-p1的规定,您只能进行以下操作:

class Foo
{
   namespace bar
   {
       ..stuff..
   }

   .. more stuff ..

   namespace bar
   {
       ..still more stuff..
   }
};

在7.3-p4版本确定前,这种功能的实用性可能进行了大约3秒钟的辩论。


1
因此,类内的命名空间将违反7.3-p4中的定义。你是不是指的是7.3-p1中的[...]? - Antonio
9
那只是一个纯粹的技术问题。你可以在可扩展性条款中添加一个例外,规定只有类作用域之外的命名空间才能被扩展。很容易做到。 - einpoklum
谢谢你的回答,知道这背后的原因很好。 - Manoj Patil
3
这个功能的实用性可能在3秒钟内进行了辩论,然后7.3-p4被确定来解决它。讨论通常不会像这样。讨论将转化为关于7.3-p4本身是否有任何意义的讨论。在没有7.3-p4的情况下,命名空间将是一种更强大的工具,这是我们在这里提出的整个论点。 - Osama Kawish

35
我要反对其他人的意见。我认为这样做确实有优势。有时候我只是想将代码分开而没有额外的影响。比如,我正在使用多线程环形缓冲区模块,并想把状态成员分割到命名空间中去,其中一些是原子和/或内存对齐的。
仅仅通过在前面加上producerconsumer前缀(这是我当前烦人的实现方式),我会增加使代码更难读的污染。例如,当所有属于生产者的东西都以producer开头时,阅读时很容易因为自动更正而把producerProducerTimer(生产者计时器的复制品)理解成producerConsumerTimer(消费者计时器的阴影)或consumerProducerTimer(生产者计时器的阴影)。这需要花费比必要更长的时间来调试,因为代码不再是可浏览的。
通过创建一个嵌套的类/结构体:
  • 我可以给下一个维护此代码的开发人员一个想法,即一个上下文中可以实例化、复制和相互赋值的不止一个这样的对象,所以现在除了担心命名之外,我还必须= delete这些东西。
  • 我可能会增加内存占用,这需要结构对齐填充,否则可能是不必要的。
  • 将所有成员设置为静态不是一个选项,因为可能会实例化多个上下文,每个都需要自己的生产者/消费者状态变量。
  • 这样的结构体函数不能再访问其他成员数据或函数,例如常量或两侧共享的函数,而必须将这些内容作为参数传递。
理想情况下,我希望能够像这样更改事物:
rbptr producerPosition;
rbptr consumerPosition;

变为:

namespace producer
{
    rbptr position;
}
namespace consumer
{
    rbptr position;
}
然后,只应涉及使用者成员的函数可以使用使用者命名空间,只应涉及生产者成员的函数可以使用生产者命名空间,需要涉及两者的函数必须显式限定它们。这样,在仅使用生产者命名空间的函数中无法意外地触及使用者变量。
在这种情况下,期望仅为减少生产者和使用者副本之间的命名冲突,并且减少命名冲突是命名空间存在的原因。因此,我支持在类内声明命名空间的建议。

8
我同意这种观点。被标记为“接受答案”的答案之所以被接受,是因为它回答了问题“为什么目前不是这种情况”,而主要答案则是“你可以使用其他功能来完成,但效果不佳,因此还没有人提出此建议”。这并不意味着它必须一直保持这种状态。如果有人提出这样的建议,我也会支持它的出现的那天 :) - Drax
1
添加get和set运算符(转换+赋值),你就拥有了属性(免费的另一个命名空间内部类/结构体的特性)。 - firda
我支持这个功能。 - Bart

9

将此功能添加到语言中实际上没有什么优势。除非有需求,否则通常不会添加功能。

类内命名空间会给你带来什么好处?你真的愿意说 binary_tree::iterator::left_depth 而不是简单地说 binary_tree::left_depth 吗?也许如果你有多个内部命名空间,你可以用它们来区分 binary_tree::depth_iterator::leftbinary_tree::breadth_iterator::right

无论如何,您都可以使用内部类作为劣质程序员的命名空间来实现所需的结果,这更加说明了为什么没有真正需要在类内部使用命名空间。


104
“除非有需求,否则通常不会添加功能。” 我们正在谈论C++,对吗? - Rag
3
在模板化时,如果一个类使用命名空间中的内容作为“它”的版本,则会很有用。例如,T::Namespace::EnumValue。 - idij
2
命名空间(Namespace)在类内外几乎完全是语法糖,那么为什么还要使用它们呢? - einpoklum
3
当你不想混淆那些名字相似但在代码中不同位置执行类似操作的变量时,使用命名空间能够帮助解决这个问题。当然,有很多方法可以实现这一点,但是在不想使用额外结构体时,在类内使用命名空间会是一个简单的解决方案。 - kushy
4
“命名空间(几乎)完全是语法糖,不管是在类内外。那么为什么还需要它们呢?”让我们回到平面、脆弱、笨拙、不完整、不灵活、过于冗长、重复的“VOLUNTARY_NAMING_CONVENTIONS”和“Static::items::for::scoping”等不合适的方式,而不是使用正确的、受编译器支持的、“语义化”的、灵活的(等等)“真正”的命名空间。 - Sz.
显示剩余4条评论

4
这不是命名空间的主要作用。命名空间的作用在于存在于代码更高层级,这样不同公司(或代码库)可以相互混合使用代码。更具体地说,我使用IMAP接收电子邮件和SMTP发送电子邮件,并且(这里进行了大量简化)两个模块中都有名为“Email”的类,它们非常不同。但是我可以拥有一个应用程序,比如一个邮件客户端,想要从同一个类中使用两者,例如转发来自一个帐户的邮件到另一个帐户。命名空间/包名称等允许此操作。

您提出的方法并不适用于命名空间的用途——在一个文件中,您可以为事物赋予不同的名称,因为作者对该文件具有全局知识,但是当两个公司想要共享代码或两个应用程序在任何时候都没有意识到会发生冲突时,情况就不同了。


3

我认为值得一提的一个小想法是在类中使用命名空间,这可以实现与模板化命名空间相同的功能。

template<class...types>
struct Namespace {
    namespace Implementation {
        ...
    }
};

// somewhere else

using namespace Namespace<types...>::Implementation;

// use templated stuff.

就我个人而言,我喜欢这个功能,但似乎需求不足以实现它。


3
这也很有用,可以在新作用域中导入嵌套枚举类型的值。 - Svalorzen
1
@Svalorzen 是的。类和命名空间非常相似,所以我不确定为什么有这么多人反对这个想法。 - Anthony
@AnthonyMonterrosa,其中一部分只是通常根深蒂固的“紧抓你所拥有的”态度,与 Poul-Henning Kamp 在经典的 bikeshedding "antirant" 中所写的非常优美的内容重叠。然后,其中一部分是改变几十年历史、庞大且已经非常复杂的 C++ 规范中的 任何 东西都很难且具有风险。 - Sz.

0

我可能会因此而受到批评

template<typename Data>
class binary_tree
{
 public:
  ... stuff ....     

 private:
  ... iterators class declaration ...

 public:
  class iterator
  {
    iterator() = delete;
  public:
    typedef left_depth_iterator_impl     left_depth;
    typedef right_depth_iterator_impl    right_depth;
    typedef left_breadth_iterator_impl   left_breadth;
    typedef right_breadth_iterator_impl  right_breadth;
  }

  ... stuff ....     

 private:
  Data         data;
  binary_tree* left;
  binary_tree* right;
};

这基本上是一个静态类,不能被实例化,所以你必须使用iterator::type来访问其中的typedefs。现在你可以将其作为常规命名空间使用。
void  function()
{
  binary_tree::iterator::left_depth   it;

  ...stuff...
}

嗨,嵌套类的选项已经在被接受的答案中提到了。无论如何,请注意,仅仅发布代码而不解释代码如何回答问题并不是一个好的做法。请编辑您的答案以解释您的建议及其优点。此外,请注意,OP在问题中明确说明他知道嵌套类。他询问原因而不是解决方法。 - Sebastian Cabot
是的,我会添加细节。 - MaxCE
但是答案给出了不同的实现方式。我已经按照他们想要的做法做了,就是在类内部创建了一个命名空间。 - MaxCE
好的,所以 OP 意识到了问题但没有提供任何代码。我回答是因为我知道有人会问同样的问题,而且可能不会读整个问题。有时候你得为别人回答。 - MaxCE
你是正确的,接受的答案建议使用嵌套类(就像你的答案一样)。但是正如你所看到的,它不是得票最多的答案。如果你想为他人展示一个好的解决方案,那没问题。正如我所写的-你需要详细说明并解释你的解决方案。 - Sebastian Cabot
我尽可能详细地阐述了,但实际上我找不到其他需要详细说明的内容。 - MaxCE

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