【问题标题】:Why does C++ need the scope resolution operator?为什么 C++ 需要范围解析运算符?
【发布时间】:2012-03-09 10:35:15
【问题描述】:

(我知道范围解析运算符的作用,以及如何以及何时使用它。)

为什么 C++ 有 :: 运算符,而不是为此目的使用 . 运算符? Java 没有单独的运算符,并且工作正常。 C++ 和 Java 之间是否存在一些差异,这意味着 C++ 需要单独的运算符才能进行解析?

我唯一的猜测是:: 是出于优先级原因需要,但我想不出它为什么需要比. 更高的优先级。我能想到的唯一情况是这样的

a.b::c;

会被解析为

a.(b::c);

,但我想不出这样的语法在任何情况下都是合法的。

也许这只是“他们做不同的事情,所以他们看起来也可能不同”的情况。但这并不能解释为什么:: 的优先级高于.。

【问题讨论】:

    标签: c++ operators language-design scope-resolution


    【解决方案1】:

    因为 C++ 标准委员会中的某个人认为允许这段代码工作是个好主意:

    struct foo
    {
      int blah;
    };
    
    struct thingy
    {
      int data;
    };
    
    struct bar : public foo
    {
      thingy foo;
    };
    
    int main()
    {
      bar test;
      test.foo.data = 5;
      test.foo::blah = 10;
      return 0;
    }
    

    基本上,它允许成员变量和派生类类型具有相同的名称。当某人认为这很重要时,我不知道他们在抽烟。但它就在那里。

    当编译器看到.时,它就知道左边的东西一定是一个对象。当它看到:: 时,它必须是一个类型名或命名空间(或者什么都没有,表示全局命名空间)。这就是它解决这种歧义的方法。

    【讨论】:

    • 为什么不能写test.foo.blah = 10?或者test.base.blah = 10 其中base 是关键字?
    • @Nawaz:因为引入关键字比引入运算符要困难很多。而test.foo.blah 是模棱两可的;是基类的blah 还是thingy 成员的blah? Java(据我所知)通过声明它始终是成员来解决这个问题。您只能通过转换类型来获取基类成员变量。
    • @Nawaz:因为这不会提供任何方式来指定 which base 你想使用。
    • @NicolBolas:从编译器作者的角度来看,这可能更难,但从程序员的角度来看,base.blah 更容易(而且不那么尴尬)。
    • @Nawaz:当然,除非您的代码曾在任何地方使用过标识符base。这是完全可能的。由于编译器,关键字并不难;这很难,因为它会使使用这些关键字的东西中断。即使是特定于上下文的关键字也意味着您不能拥有名为 base 的类型。
    【解决方案2】:

    为什么 C++ 在使用 :: 的地方不使用 .,因为这就是语言的定义方式。一个合理的原因可能是,使用语法::a 来引用全局命名空间,如下所示:

    int a = 10;
    namespace M
    {
        int a = 20;
        namespace N
        {
               int a = 30;
               void f()
               {
                  int x = a; //a refers to the name inside N, same as M::N::a
                  int y = M::a; //M::a refers to the name inside M
                  int z = ::a; //::a refers to the name in the global namespace
    
                  std::cout<< x <<","<< y <<","<< z <<std::endl; //30,20,10
               }
        }
    }
    

    Online Demo

    我不知道 Java 是如何解决这个问题的。我什至不知道在 Java 中是否有全局命名空间。在 C# 中,您使用语法 global::a 来引用全局名称,这意味着即使 C# 也有 :: 运算符。


    但我想不出这样的语法在任何情况下都是合法的。

    谁说a.b::c 这样的语法不合法?

    考虑这些类:

    struct A
    {
        void f() { std::cout << "A::f()" << std::endl; }
    };
    
    struct B : A
    {
        void f(int) { std::cout << "B::f(int)" << std::endl; }
    };
    

    现在看到这个(ideone):

    B b;
    b.f(10); //ok
    b.f();   //error - as the function is hidden
    

    b.f() 不能这样调用,因为该函数是隐藏的,并且 GCC 会给出以下错误消息:

    error: no matching function for call to ‘B::f()’
    

    为了调用b.f()(或者更确切地说A::f()),您需要范围解析运算符:

    b.A::f(); //ok - explicitly selecting the hidden function using scope resolution
    

    Demo at ideone

    【讨论】:

    • 这并不能解释为什么你不能只说b.A.f 而不是b.A::f。如果A 是类型名而不是变量或函数,那么使用. 很容易意味着scope resolution 而不是常规含义。
    • 您仍然可以只说.a 表示全局,M.a 表示M 命名空间中的那个。
    • 也许可以,但如果您已经研究了 10 年,就不会了。就个人而言,:: 即使在 10 年后看起来也更尴尬。
    • 我对@9​​87654347@ 不明智的假设是造成我困惑的原因。接受这个答案是因为我认为它和其他答案一样好,但也指出了我的错误。
    • Java 中没有全局命名空间,因为一切都在一个或另一个类中。
    【解决方案3】:

    为什么 C++ 有 :: 运算符,而不是使用 .运营商为此目的?

    原因由 Stroustrup 本人给出:

    在带有类的 C 中,点用于表示类的成员资格以及表示对特定对象成员的选择。

    这引起了一些轻微的混淆,也可以用来构建模棱两可的例子。为了缓解这个问题,引入了:: 来表示类成员,而. 专门用于对象成员

    (Bjarne Stroustrup A History of C++: 1979−1991 第 21 页 - § 3.3.1)

    而且确实

    他们做不同的事情,所以他们可能看起来不同

    确实

    在N::m 中,N 和 m 都不是带值的表达式; N 和 m 是编译器已知的名称,:: 执行(编译时)范围解析而不是表达式求值。可以想象允许 x::y 重载,其中 x 是一个对象,而不是命名空间或类,但这将 - 与第一次出现相反 - 涉及引入新语法(以允许 expr::expr)。这种并发症会带来什么好处尚不清楚。

    运算符.(点)原则上可以使用与-&gt; 相同的技术进行重载。

    (Bjarne Stroustrup 的C++ Style and Technique FAQ)

    【讨论】:

      【解决方案4】:

      与 Java 不同,C++ 具有多重继承。这是一个示例,您所说的范围解析变得很重要:

      #include <iostream>
      using namespace std;
      struct a
      {
          int x;
      };
      struct b
      {
          int x;
      };
      struct c : public a, public b
      {
          ::a a;
          ::b b;
      };
      int main() {
          c v;
          v.a::x = 5;
          v.a.x = 55;
          v.b::x = 6;
          v.b.x = 66;
          cout << v.a::x << " " << v.b::x << endl;
          cout << v.a.x << " " << v.b.x << endl;
          return 0;
      }
      

      【讨论】:

      • 从技术上讲,这与多重继承无关。这是为了能够将变量命名为与派生类相同的名称。
      • C++ 在引入:: 运算符时没有多重继承。请参阅Cfront E manual, page 22 (25 in the pdf) - :: 正在使用中,但在类的描述中没有多重继承的迹象。
      【解决方案5】:

      只是回答关于运算符优先级问题的最后一点:

      class A {
      public:
        char A;
      };
      
      class B : public A {
      public:
        double A;
      };
      
      int main(int c, char** v)
      {
        B myB;
        myB.A = 7.89;
        myB.A::A = 'a';
        // On the line above a hypothetical myB.A.A
        // syntax would parse as (myB.A).A and since
        // (myB.A) is of type double you get (double).A in the
        // next step. Of course the '.' operator has no
        // meaning for doubles so it causes a syntax error. 
        // For this reason a different operator that binds
        // more strongly than '.' is needed.
        return 0;
      }
      

      【讨论】:

      • 你是说解析器不能简单地等待并检查下一个标记,看看它是否是.,然后再决定歧义?
      • 不,当然可以编写这样的解析器。中间结果将是模棱两可的,当下一个标记出现时,您可以假设用户并不是要犯语法错误。所以从这个意义上说,它并不是绝对必要的,但是 '::' 运算符在其他地方很有用,C++ 解析器的作者已经有足够的问题了。 :)
      • @nolandda C++ 的第一个版本“Cpre”有一个工作解析器,它使用. 进行范围解析(参见 Cfront 的源代码,它是由 Cpre 编译的:softwarepreservation.org/projects/c_plus_plus/cfront/release_e/… )。在弄清楚如何做之后,我怀疑 Stroustrup 在重新实现该语言时会不会再做这件事。因此,我不认为使用它的技术难度是部分原因。
      【解决方案6】:

      我一直认为 C++ dot/:: 用法是一种风格选择,以使代码更易于阅读。正如 OP 所写的那样“他们做不同的事情,所以看起来应该不同。”

      从很久以前的 C++ 到 C#,我发现只使用点令人困惑。我看惯了A::doStuff();B.doStuff();,知道第一个是命名空间中的常规函数​​,第二个是实例B上的成员函数。

      C++ 可能是我在 Basic、汇编、Pascal 和 Fortran 之后的第五种语言,所以我不认为它是第一语言综合症,我现在更像是一名 C# 程序员。但是,恕我直言,如果您同时使用了两者,命名空间的 C++ 样式双冒号会更好读。我觉得 Java/C# 为两者都选择了点来(成功地)缓解学习曲线的前端。

      【讨论】:

        【解决方案7】:

        作用域解析运算符(::) 用于定义类外的函数,或者当我们想使用全局变量但又具有同名的局部变量时。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2011-02-23
          • 2012-04-20
          • 2011-07-28
          • 2010-09-09
          • 1970-01-01
          • 2014-10-05
          • 2015-12-18
          相关资源
          最近更新 更多