关于c ++:为什么在非const的私有时不调用公共const方法?

Why is a public const method not called when the non-const one is private?

考虑此代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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();
}

编译器错误为:

error: 'void A::foo()' is private`.

但是当我删除了这个私有的,它就工作了。当非常量方法是私有的时,为什么不调用公共常量方法?

换句话说,为什么超负荷解决方案比访问控制来得早?这很奇怪。你认为这是一致的吗?我的代码可以工作,然后我添加了一个方法,我的工作代码根本不编译。


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

1
void foo() const

1
void foo()

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

记住,在过载分辨率中,它不是"找到最佳可用函数"。它是"找到最好的功能并尝试使用它"。如果由于访问限制或被删除而无法执行,则会出现编译器错误。

In other words why does overload resolution comes before access control?

好吧,让我们看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct Base
{
    void foo() { std::cout <<"Base
"
; }
};

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

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。这在大型代码库中可能很难找到。由于访问控制是在重载解析之后出现的,所以我得到了一个很好的编译器错误,告诉我要调用的函数不能被调用,而且我可以更容易地发现这个bug。


最终,这归结于标准中的断言,即在执行过载解决方案时,不应考虑可访问性。此断言可在[over.match]第3条中找到:

... When overload resolution succeeds, and the best viable function is not accessible (Clause [class.access]) in the context in which it is used, the program is ill-formed.

以及同一节第1条中的注释:

[ Note: The function selected by overload resolution is not guaranteed to be appropriate for the context. Other restrictions, such as the accessibility of the function, can make its use in the calling context ill-formed. — end note ]

至于原因,我可以考虑几个可能的动机:

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

  • 假设访问控制早于过载解决方案。实际上,这意味着public/protected/private控制了可见性,而不是可访问性。

    Stroustrup的C++设计和演化的第2.10节在这里讨论了下面的例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int a; // global a

    class X {
    private:
        int a; // member X::a
    };

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

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

    他接着说,他不记得它是否是通过显式设计或副作用的预处理器技术用于实现C与前级标准C + +。

    这与您的示例有什么关系?基本上是因为标准使重载解决方案符合一般规则,即名称查找先于访问控制。

    10.2 Member name lookup [class.member.lookup]

    1 Member name lookup determines the meaning of a name (id-expression)
    in a class scope (3.3.7). Name lookup can result in an ambiguity, in
    which case the program is ill-formed. For an id-expression, name
    lookup begins in the class scope of this; for a qualified-id, name
    lookup begins in the scope of the nestedname- specifier. Name lookup
    takes place before access control (3.4, Clause 11).

    8 If the name of an overloaded function is unambiguously found,
    overloading resolution (13.3) also takes place before access control.
    Ambiguities can often be resolved by qualifying a name with its class
    name.


    由于隐式this指针是非const指针,编译器将首先检查该函数在const版本之前是否存在非const版本。

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


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

  • 找到所有可行的函数。
  • 选择最可行的功能。
  • 如果没有一个最佳可行函数,或者您实际上无法调用最佳可行函数(由于访问冲突或函数为deleted),则失败。
  • (3)发生在(2)之后。这一点非常重要,因为否则使函数deleted或private变得毫无意义,更难以解释。

    在这种情况下:

  • 可行的功能是A::foo()A::foo() const
  • 最可行的函数是A::foo(),因为后者涉及隐式this参数的限定转换。
  • 但是A::foo()private,您没有访问权,因此代码格式不正确。

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

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

  • 它会搜索第一个作用域,在这个作用域中有一个同名的东西。

  • 编译器在该范围内查找具有该名称的所有函数(或函数等)。

  • 然后编译器进行重载解析,以在找到的候选项中找到最佳候选项(无论它们是否可访问)。

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

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

    至于是否可以做不同的事情:是的,这无疑是可能的。它肯定会导致与C++完全不同的语言。事实证明,许多看似非常微小的决定可能会产生比最初明显的影响更大的后果。

  • "第一个"本身可能有点复杂,尤其是当/如果涉及到模板时,因为它们可能导致两个阶段的查找,这意味着在执行搜索时有两个完全独立的"根"。不过,其基本思想相当简单:从最小的封闭范围开始,然后向外扩展到更大的封闭范围。

  • 访问控制(publicprotectedprivate不影响过载分辨率。编译器选择void foo(),因为它是最佳匹配。它不可访问的事实并没有改变这一点。去掉它只剩下void foo() const,这是最好的(即只有)匹配。


    在这个电话里:

    1
    a.foo();

    在每个成员函数中总是有一个隐式this指针可用。而thisconst资格则取自调用的引用/对象。编译器将上述调用视为:

    1
    A::foo(a);

    但你有两份关于A::foo的声明,其处理方式如下:

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

    通过过载决议,第一个将被选为非常数dOCx1〔3〕,第二个将被选为非常数dOCx1〔14〕。如果删除第一个,第二个将绑定到constnon-constthis

    过载解决后,选择最佳可行的功能,来进行访问控制。由于您将对所选重载的访问指定为private,编译器随后将发出抱怨。

    标准规定:

    [class.access/4]: ...In the case of overloaded function names, access control is applied to
    the function selected by overload resolution....

    但如果你这样做:

    1
    2
    3
    A a;
    const A& ac = a;
    ac.foo();

    然后,只适合const过载。


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

    In other words why overload resolution comes before access control? This is strange. Do you think it is consistent? My code works and then I add a method and my working code does not compile at all.

    语言就是这样设计的。其目的是尽可能称之为最佳可行过载。如果失败,将触发一个错误,提醒您重新考虑设计。

    另一方面,假设您的代码经过编译,并与被调用的const成员函数一起工作良好。有一天,有人(也许是你自己)决定将非const成员函数的可访问性从private改为public。然后,行为将改变,不会有任何编译错误!这将是一个惊喜。


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

    这样,如果更改访问说明符,则在编译时,如果现有代码中存在冲突,您将收到警报;如果在函数调用解析时考虑到隐私,则程序的行为可能会自动更改。


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

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