当非const方法是私有方法时,为什么不调用public const方法?


117

考虑以下代码:

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方法为私有方法时,为什么不调用public const方法?

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


3
在C ++中,无需像使用PIMPL习惯用法那样付出额外的努力,该类就没有真正的“私有”部分。这只是由它引起的问题之一(在我的书中增加了一个“私有”方法重载并破坏了编译旧代码,这是我的书中的一个问题,即使通过不这样做就可以避免这种麻烦)。
海德

是否有任何现实的代码可以调用const函数,但其​​非const对应项将成为私有接口的一部分?这对我来说听起来像是不良的界面设计。
文森特·福尔蒙德

Answers:


125

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

void foo() const

void foo()

现在,由于a不是const,因此非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 '16

3
@ drake7707如我在代码示例中所示,如果访问控制首先出现,则上面的代码将被编译,这将更改程序的语义。不确定您的情况,但是如果我希望函数保持私有状态,则隐式强制转换,并且代码可以“正常工作”,如果我希望函数保持私有状态,我宁愿出错并需要执行显式强制转换。
NathanOliver '16

“并且,如果我希望函数保持私有状态,则需要进行显式转换”-听起来这里的真正问题是隐式转换...尽管另一方面,您也可以隐式使用派生类的想法基类是OO范式的定义特征,不是吗?
史蒂文·比克斯

35

最终,这归结为标准中的断言,即执行重载解析时不应考虑可访问性。可以在[over.match]子句3中找到此断言:

...如果重载解析成功,并且在使用它的上下文中无法访问最佳可行功能(Clause [class.access]),则程序格式错误。

以及同一部分第1条中的注释

[注意:不保证通过重载解析选择的功能适合于上下文。其他限制,例如功能的可访问性,可能会使它在格式错误的调用上下文中使用。—尾注]

至于原因,我可以想到一些可能的动机:

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

32

假设访问控制先于重载解决方案进行。实际上,这将意味着public/protected/private控制可见性而不是可访问性。

Stroustrup撰写的C ++设计和演变的 2.10节对此进行了一段论述,他在其中讨论了以下示例

int a; // global a

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

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

斯特劳斯提到的现行规则(能见度可达前)一个好处是,(暂时)chaning的private内部class X进入public(例如用于调试的目的)是有在上述程序的含义没有安静的变化(即X::a是试图在两种情况下都可以被访问,这在上面的示例中给出了访问错误)。如果public/protected/private将控制可见性,则程序的含义将发生变化(a将使用调用全局private,否则X::a)。

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

这与您的示例有何关系?基本上是因为Standard做出的重载解析符合访问规则之前先进行名称查找的一般规则。

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

1成员名称查找确定类范围(3.3.7)中名称(id-表达式)的含义。名称查找可能导致歧义,在这种情况下,程序格式不正确。对于id表达式,名称查找始于此类的范围;对于限定ID,名称查找始于nestedname-specifier的范围。名称查找发生在访问控制之前(3.4,第11条)。

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


23

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

如果你明确地标记非const一个private则解析将失败,编译器将不会继续搜索。


您认为这是一致的吗?我的代码正常工作,然后添加一个方法,而我的工作代码根本无法编译。
Narek

我确实是这样。过载解析是有目的的。昨天我回答了类似的问题:stackoverflow.com/questions/39023325/...
拔示巴

5
@Narek我相信它的工作原理与删除函数在重载解析中的工作方式一样。它从集合中选择最佳,然后发现它不可用,因此会出现编译器错误。它不会选择最佳的可用功能,而是会选择最佳的功能,然后尝试使用它。
NathanOliver '16

3
@Narek我也首先想知道为什么它不起作用,但是请考虑一下:如果还应该为非const对象选择public const,那么您将如何调用私有函数?
idclev 463035818 '16

20

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

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

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

在这种情况下:

  1. 可行的功能是A::foo()A::foo() const
  2. 最好的可行函数是A::foo()因为后者涉及隐式this参数的条件转换。
  3. 但是A::foo()private,您无权访问它,因此代码格式错误。

1
可能有人认为“可行”将包括相关的访问限制。换句话说,从类外部调用私有函数不是“可行的”,因为它不是该类的公共接口的一部分。
RM

15

这取决于C ++中相当基本的设计决策。

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

  1. 它搜索以找到第一个1范围,在该范围内有该名称的东西

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

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

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

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

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


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

1
Stroustrup在D&E中推测,该规则可能是C语言中使用带有类的预处理器的副作用,而一旦获得更高级的编译器技术,该预处理器就永远不会得到审查。看我的回答
TemplateRex

12

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


11

在此通话中:

a.foo();

this每个成员函数中始终都有一个隐式指针。的const限定条件this来自调用参考/对象。编译器将上述调用视为

A::foo(a);

但是您有两个声明A::foo视为

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

通过重载解析,将为非常量选择第一个this,为a选择第二个const this。如果删除第一个,则第二个将同时绑定到constnon-const this

经过过载解析以选择最佳的可行功能后,进行访问控制。由于您将访问权限指定为private,因此编译器将发出抱怨。

该标准是这样说的:

[class.access / 4] ...在函数名称重载的情况下,访问控制将应用于通过重载解析选择的函数。

但是,如果您这样做:

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

然后,只有const过载才适合。


这是在过载解析后选择最佳可行功能的 STRANGE ,来进行访问控制。访问控制应该在过载解决之前进行,就好像您没有访问权限,您应该根本不考虑它一样,您的想法是什么?
纳雷克

@Narek,..我已经用对C ++标准的引用更新了答案。这样实际上就很有意义,C ++中有很多事情和习惯用法都取决于这种行为
WhiZTiM

9

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

换句话说,为什么在访问控制之前要先解决过载问题?这很奇怪。您认为这是一致的吗?我的代码正常工作,然后添加一个方法,而我的工作代码根本无法编译。

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

另一方面,假设您的代码已编译,并且可以与const调用的成员函数一起很好地工作。有一天,某人(也许是您自己)决定将非const成员函数的可访问性从private更改为public。然后,行为将改变而没有任何编译错误!这将是一个惊喜



8

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

这样,如果您更改访问说明,则在编译时会警告您现有代码中是否存在冲突。如果在函数调用解析中考虑了隐私,则程序的行为可能会悄无声息地发生变化。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.