为什么当非const成员函数是私有的时候,公共const成员函数不被调用?

123
考虑一下这段代码:
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函数是私有的时候,公共的const成员函数没有被调用呢?
换句话说,为什么重载解析在访问控制之前?这很奇怪。你认为这是一致的吗?我的代码本来可以工作,然后我添加了一个成员函数,我的工作代码完全无法编译。

3
在C++中,如果没有采用PIMPL编程技巧等额外的努力,类中就不存在真正的“私有”部分。这只是其中的一个问题(例如增加“私有”方法重载并破坏旧代码的编译在我看来也算是一个问题,即使避免这个问题非常简单,只需要不这样做)。 - hyde
有没有任何真实的代码,你期望能够调用一个const函数,但它的非const对应函数却是私有接口的一部分?这听起来像是糟糕的接口设计。 - Vincent Fourmond
11个回答

128

当你调用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。在一个庞大的代码库中,这可能很难追踪。由于访问控制在重载分辨率之后执行,我可以得到一个很好的编译器错误提示,告诉我无法调用我想要调用的函数,因此我可以更容易地找到错误。


1
访问控制为什么要在重载解析之后呢? - Drakarah
3
正如我在代码示例中所展示的那样,如果访问控制是首要考虑的话,那么上面的代码将编译通过,这将改变程序的语义。不确定你是否同意,但我宁愿出现错误并需要进行显式转换,如果我希望函数保持私有,而不是进行隐式转换并且代码默默地“工作”。 - NathanOliver
“而且如果我想让函数保持私有,就需要进行显式转换” - 这听起来真正的问题是隐式转换...不过另一方面,衍生类也可以隐式地作为基类使用,这恰恰是面向对象范例的一个定义特征,是吧? - Steven Byks

38

归根结底,这与标准中的断言有关:在进行重载决议时,不应考虑可访问性。该断言可以在[over.match]第3条款中找到:

……当重载决议成功,并且最佳可行函数在使用它的上下文中是不可访问的(第[class.access]节),程序就是不合法的。

还有同一部分第1条款中的注意事项

[注意:重载解析选择的函数不能保证适用于上下文。其他限制,例如函数的可访问性,可能会使其在调用上下文中不合法。—end note]

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

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

34
假设访问控制在重载解析之前。实际上,这意味着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控制可见性,则程序的含义将发生变化(使用private调用全局a,否则调用X::a)。

他随后表示,他不记得这是通过明确设计还是作为实现C++之前的类C的预处理器技术的副作用而实现的。

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

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

1 成员名称查找决定了类作用域(3.3.7)中名称(id-expression)的含义。名称查找可能导致歧义,此时程序是非法的。对于 id-expression,名称查找从 this 的类作用域开始;对于 qualified-id,名称查找从嵌套名称限定符的作用域开始。访问控制之前进行名称查找(3.4,第11条款)。

8 如果多个重载函数的名称可以明确地找到,在访问控制之前也会进行重载决议(13.3)。通常可以通过使用类名限定名称来解决歧义。


23

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

如果你明确将非const版本标记为private,则解析过程将失败,编译器将不会继续搜索。


你认为这是否一致?我的代码能够正常运行,但是当我添加一个方法后,原本可以工作的代码却完全无法编译。 - Narek
我也这么认为。重载决议是故意模糊的。我昨天回答了一个类似的问题:https://dev59.com/ypnga4cB1Zd3GeqPgPh5#39023634 - Bathsheba
5
我认为它的工作方式与重载决议中的已删除函数相同。它从集合中选择最好的一个,然后发现它无法使用,因此您会得到编译器错误。它不会选择最佳可用函数,而是选择最佳函数,然后尝试使用它。 - NathanOliver
3
@Narek 我也一开始想知道为什么它不起作用,但请考虑这一点:如果公共const函数应该被选择用于非const对象,那么你将如何调用私有函数呢? - 463035818_is_not_a_number

21

记住这一系列事件的顺序非常重要,顺序如下:

  1. 找出所有可行的函数。
  2. 选择最佳的可行函数。
  3. 如果没有确切的最佳可行函数,或者无法调用最佳可行函数(由于访问违规或函数被delete),则失败。

(3)发生在(2)之后。这非常重要,否则使函数成为deleted或private将变得毫无意义,而且难以理解。

在这种情况下:

  1. 可行函数是A::foo()A::foo() const
  2. 最佳可行函数是A::foo(),因为后者涉及隐式this参数的资格转换。
  3. 但是A::foo()private的,您无法访问它,因此代码不符合规范。

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

15

这涉及到C++中一个相当基础的设计决策。

在查找满足调用的函数时,编译器会进行如下搜索:

  1. 它搜索以找到第一个1具有该名称的作用域中的某些内容。

  2. 编译器在该作用域中找到该名称的所有函数(或函数对象等)。

  3. 然后,编译器执行重载决议,从找到的候选项中找到最佳的一个(无论它们是否可访问)。

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

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

至于是否可以以不同的方式完成操作:是的,毫无疑问地可以。但这肯定会导致与C++非常不同的语言。事实证明,许多看似相当微小的决策都会产生影响,影响远比起初显而易见的要大得多。


  1. "First"本身可能有点复杂,特别是当/如果涉及到模板时,因为它们可能导致两阶段查找,这意味着在进行搜索时有两个完全独立的“根”可以开始。不过,基本思想非常简单:从最小封闭作用域开始,向外逐渐扩大到更大的封闭作用域。

1
斯特鲁斯特鲁普在《D&E》中猜测这个规则可能是C++中使用的预处理器的副作用,由于更先进的编译器技术已经可用,所以从未再次进行过审查。 参见我的回答 - TemplateRex

12

访问控制(publicprotectedprivate)不影响重载决策。编译器选择void foo()是因为它是最佳匹配。它不可访问的事实并不改变这一点。删除它只剩下void foo() const,然后就成为了最佳(也是唯一)匹配。


11

在这个调用中:

a.foo();

每个成员函数中都会隐式地有一个 this 指针可用。而 thisconst 限定符是从调用的引用 / 对象中获取的。编译器将上述调用视为:

A::foo(a);

但你有两个声明 A::foo,这被视为

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

通过重载解析,第一个函数将被选中为非常量 this,第二个函数将被选中为 const this。如果你去掉第一个函数,则第二个函数将绑定到 const非constthis 上。

在选择最佳可行函数后进行访问控制。由于你指定了所选重载的访问权限为 private,因此编译器会发出警告。

标准如下:

[class.access/4]: ...对于重载函数名称,访问控制应用于通过重载解析选择的函数....

但如果你这样做:

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

那么,只有const重载才会被匹配。


很奇怪,在重载解析选择最佳可行函数之后,出现了访问控制。访问控制应该在重载解析之前,因为如果您没有访问权限,您根本不应该考虑它,您认为呢? - Narek
@Narek,我已经更新了我的答案,并附上了对C++标准的参考链接(http://eel.is/c++draft/class.access#4)。这样做实际上是有道理的,因为C++中有很多东西和习惯用法取决于这种行为。 - WhiZTiM

9
技术原因已经在其他答案中回答,我只关注这个问题:
换句话说为什么过载解析(overload resolution)在访问控制(access control)之前?这很奇怪。你认为这是一致的吗?我的代码有效,然后我添加一个方法,我的有效代码完全无法编译。
这就是语言的设计初衷。意图是尽可能调用最佳可行的重载。如果失败,则会触发错误以提醒您重新考虑设计。
另一方面,假设您的代码编译并成功调用了const成员函数。有一天,有人(也可能是你自己)决定将非const成员函数的可访问性从private更改为public。然后,行为会发生改变,而不会有任何编译错误!这将是一个惊喜。

8

因为在main函数中,变量a未声明为const

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


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