【问题标题】:Why is a public const method not called when the non-const one is private?为什么非 const 方法是私有的时不调用公共 const 方法?
【发布时间】:2016-12-26 19:17:12
【问题描述】:

考虑这段代码:

struct A
{
    void foo() const
    {
        std::cout << "const" << std::endl;
    }

    private:

        void foo()
        {
            std::cout << "non - const" << std::endl;
        }
};

int main()
{
    A a;
    a.foo();
}

编译错误是:

错误:“void A::foo()”是私有的。

但是当我删除私人的时,它就可以工作了。为什么非 const 方法是 private 时不调用 public const 方法?

换句话说,为什么重载解决在访问控制之前?这很奇怪。你认为它是一致的吗?我的代码可以工作,然后我添加了一个方法,我的工作代码根本无法编译。

【问题讨论】:

  • 在 C++ 中,如果没有像使用 PIMPL 成语那样的额外努力,类中就没有真正的“私有”部分。这只是它引起的问题之一(添加“私有”方法重载和破坏编译旧代码在我的书中被视为一个问题,即使这个问题通过不这样做很容易避免)。
  • 是否有任何现实生活中的代码您希望能够调用 const 函数但其非 const 对应物将成为私有接口的一部分?对我来说,这听起来像是糟糕的界面设计。

标签: c++ overloading overload-resolution private-methods const-method


【解决方案1】:

当您调用a.foo(); 时,编译器会通过重载解析来找到要使用的最佳函数。当它构建它发现的重载集时

void foo() const

void foo()

现在,由于a 不是const,因此非常量版本是最佳匹配,因此编译器选择void foo()。然后设置访问限制,您会收到编译器错误,因为void foo() 是私有的。

请记住,在重载决议中,它不是“找到最佳可用函数”。它是“找到最好的功能并尝试使用它”。如果由于访问限制或被删除而无法访问,则会出现编译器错误。

换句话说,为什么重载解析要先于访问控制?

好吧,让我们看看:

struct Base
{
    void foo() { std::cout << "Base\n"; }
};

struct Derived : Base
{
    void foo() { std::cout << "Derived\n"; }
};

struct Foo
{
    void foo(Base * b) { b->foo(); }
private:
    void foo(Derived * d) { d->foo(); }
};

int main()
{
    Derived d;
    Foo f;
    f.foo(&d);
}

现在假设我实际上并没有打算将void foo(Derived * d) 设为私有。如果首先出现访问控制,则该程序将编译并运行,并打印Base。这在大型代码库中可能很难追踪。由于访问控制是在重载解决之后出现的,所以我得到一个很好的编译器错误,告诉我无法调用我想要它调用的函数,我可以更容易地找到错误。

【讨论】:

  • 在重载解决之后访问控制有什么原因吗?
  • @drake7707 正如我在代码示例中向您展示的那样,如果访问控制首先出现,那么上面的代码将编译,这会改变程序的语义。不确定你,但如果我希望函数保持私有然后隐式转换并且代码静默“工作”,我宁愿有一个错误并且需要进行显式转换。
  • “如果我想让函数保持私有,则需要进行显式转换” - 听起来这里真正的问题是隐式转换......虽然另一方面,你可以也隐式使用派生类作为基类是 OO 范式的定义特征,不是吗?
【解决方案2】:

归根结底,这归结为标准中的断言,即在执行重载解析时不应考虑可访问性。这个断言可以在[over.match] 第 3 条中找到:

...当重载解析成功,并且最佳可行函数在使用它的上下文中不可访问(子句 [class.access])时,程序是非良构的。

以及同一节第1条中的注意

[ 注意:重载决议选择的函数不能保证适合上下文。其他限制,例如函数的可访问性,可能使其在调用上下文中的使用格式不正确。 ——尾注]

至于为什么,我可以想到几个可能的动机:

  1. 它可以防止由于更改重载候选者的可访问性而导致意外的行为更改(相反,会发生编译错误)。
  2. 它从重载解析过程中移除了上下文相关性(即重载解析无论在类内部还是外部都会产生相同的结果)。

【讨论】:

    【解决方案3】:

    假设访问控制出现在重载解决之前。实际上,这意味着public/protected/private 控制可见性而不是可访问性。

    Design and Evolution of C++ by Stroustrup 的第 2.10 节有一段关于此的文章,他讨论了以下示例

    int a; // global a
    
    class X {
    private:
        int a; // member X::a
    };
    
    class XX : public X {
        void f() { a = 1; } // which a?
    };
    

    Stroustrup 提到当前规则的一个好处(可访问性之前的可见性)是(暂时)将class X 中的private 更改为public(例如,出于调试目的)是没有安静的变化在上述程序的含义中(即在这两种情况下都试图访问X::a,这在上述示例中给出了访问错误)。如果public/protected/private 将控制可见性,则程序的含义将改变(全局a 将使用private 调用,否则X::a)。

    然后他说他不记得这是通过显式设计还是预处理器技术的副作用,该技术用于使用标准 C++ 的前身 Classess 实现 C。

    这与您的示例有何关系?基本上是因为标准使重载解析符合一般规则,即名称查找在访问控制之前。

    10.2 成员名称查找 [class.member.lookup]

    1 成员名称查找确定名称的含义(id-expression) 在类范围(3.3.7)中。名称查找可能会导致歧义,在 在这种情况下,程序格式错误。对于 id 表达式,名称 查找从 this 的类范围开始;对于限定 ID,名称 查找在nestedname-说明符的范围内开始。 名称查询 发生在访问控制之前(3.4,第 11 条)。

    8 如果明确找到重载函数的名称, 重载解决方案 (13.3) 也发生在访问控制之前。 歧义通常可以通过使用其类限定名称来解决 名字。

    【讨论】:

      【解决方案4】:

      由于隐式this 指针不是const,编译器将首先检查是否存在非const 版本的函数,然后再检查const 版本。

      如果将非const显式标记为private,则解析失败,编译器不会继续搜索。

      【讨论】:

      • 你觉得一致吗?我的代码可以工作,然后我添加了一个方法,但我的工作代码根本无法编译。
      • 我确实这么认为。重载分辨率是故意挑剔的。我昨天回答了一个类似的问题:stackoverflow.com/questions/39023325/…
      • @Narek 我相信它的工作原理就像删除函数在重载解析中的作用一样。它从集合中选择最好的一个,然后它发现它不可用,所以你得到一个编译器错误。它不会选择最好的可用功能,而是选择最好的功能,然后尝试使用它。
      • @Narek 我也首先想知道为什么它不起作用,但考虑一下:如果还应该为非 const 对象选择公共 const 函数,你将如何调用私有函数?
      【解决方案5】:

      记住事情发生的顺序很重要,即:

      1. 找出所有可行的函数。
      2. 选择最佳可行函数。
      3. 如果没有一个最佳可行函数,或者如果您实际上不能调用最佳可行函数(由于访问冲突或函数为deleted),则失败。

      (3) 发生在 (2) 之后。这真的很重要,因为否则使函数deleted 或private 变得毫无意义并且更难以推理。

      在这种情况下:

      1. 可行的函数是A::foo()A::foo() const
      2. 最佳可行函数是A::foo(),因为后者涉及隐式this 参数的限定转换。
      3. 但是A::foo()private 并且您无权访问它,因此代码格式不正确。

      【讨论】:

      • 人们可能认为“可行”包括相关的访问限制。换句话说,从类外部调用私有函数是不“可行的”,因为它不是该类的公共接口的一部分。
      【解决方案6】:

      这归结为 C++ 中一个相当基本的设计决策。

      当查找函数以满足调用时,编译器会执行如下搜索:

      1. 它会搜索第一个1 范围,在该范围内有 something 具有该名称。

      2. 编译器会在该范围内找到所有具有该名称的函数(或仿函数等)。

      3. 然后编译器执行重载决策以在它找到的那些中找到最佳候选者(无论它们是否可访问)。

      4. 最后,编译器检查所​​选函数是否可访问。

      由于这种排序,是的,编译器可能会选择一个不可访问的重载,即使存在另一个可访问的重载(但在重载解析期间未选择)。

      至于是否可能以不同的方式做事:是的,这无疑是可能的。不过,它肯定会导致与 C++ 完全不同的语言。事实证明,许多看似微不足道的决定可能会产生比最初显而易见的影响更大的影响。


      1. “第一”本身可能有点复杂,尤其是当/如果涉及模板时,因为它们可能导致两阶段查找,这意味着在进行搜索时有两个完全独立的“根”开始。 basic 的想法非常简单:从最小的封闭范围开始,然后逐步向外延伸到越来越大的封闭范围。

      【讨论】:

      • Stroustrup 在 D&E 中推测该规则可能是 C 中使用的预处理器的副作用,而一旦更高级的编译器技术可用,这些类就再也没有得到审查。 See my answer.
      【解决方案7】:

      访问控制(publicprotectedprivate)不会影响重载解决方案。编译器选择void foo(),因为它是最佳匹配。它不可访问的事实并没有改变这一点。删除它只留下void foo() const,这是最好的(即唯一的)匹配。

      【讨论】:

        【解决方案8】:

        在本次通话中:

        a.foo();
        

        每个成员函数中总是有一个隐式的this 指针可用。并且thisconst 资格来自调用引用/对象。上面的调用被编译器处理为

        A::foo(a);
        

        但是您有两个A::foo 声明,它们被视为

        A::foo(A* );
        A::foo(A const* );
        

        通过重载决议,第一个将被选择为非常量this,第二个将被选择为const this。如果删除第一个,第二个将绑定到 constnon-const this

        在重载解决方案以选择最佳可行功能之后,是访问控制。由于您将所选重载的访问权限指定为private,因此编译器会报错。

        标准是这样说的:

        [class.access/4]: ...在重载函数名的情况下,访问控制应用于 重载决议选择的函数....

        但如果你这样做:

        A a;
        const A& ac = a;
        ac.foo();
        

        那么,只有const 重载会适合。

        【讨论】:

        • 这很奇怪,在重载决议以选择最佳可行功能之后,访问控制。访问控制应该在重载解决之前出现,就好像你没有访问权限一样,你根本不应该考虑它,你怎么看?
        • @Narek ,.. 我已将 a reference 的答案更新为 C++ 标准。这实际上是有道理的,C++ 中有很多东西和习语都依赖于这种行为
        【解决方案9】:

        其他答案已经回答了技术原因。我只关注这个问题:

        换句话说,为什么重载解析要先于访问控制?这很奇怪。你认为它是一致的吗?我的代码可以工作,然后我添加了一个方法,但我的工作代码根本无法编译。

        这就是语言的设计方式。目的是尽可能地调用最佳可行的重载。如果失败,则会触发错误,提醒您重新考虑设计。

        另一方面,假设您的代码已编译并在调用const 成员函数时运行良好。有一天,某人(可能是您自己)然后决定将非const 成员函数的可访问性从private 更改为public。然后,行为会改变而没有任何编译错误!这将是一个惊喜

        【讨论】:

          【解决方案10】:

          因为main函数中的变量a没有声明为const

          在常量对象上调用常量成员函数。

          【讨论】:

            【解决方案11】:

            访问说明符永远不会影响名称查找和函数调用解析。在编译器检查调用是否应该触发访问冲突之前选择该函数。

            这样,如果您更改访问说明符,如果现有代码中存在违规,您将在编译时收到警报;如果在函数调用解析中考虑到隐私,您的程序的行为可能会悄无声息地改变。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2016-04-16
              • 2012-01-09
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-06-21
              • 2018-07-02
              相关资源
              最近更新 更多