Warum wird eine öffentliche const-Methode nicht aufgerufen, wenn die nicht-const-Methode privat ist?

117

Betrachten Sie diesen Code:

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();
}

Der Compilerfehler ist:

Fehler: 'void A :: foo ()' ist privat`.

Aber wenn ich die private lösche, funktioniert es einfach. Warum wird die public const-Methode nicht aufgerufen, wenn die non-const-Methode privat ist?

Mit anderen Worten, warum kommt die Überlastungsauflösung vor der Zugriffskontrolle? Das ist merkwürdig. Denken Sie, dass es konsequent ist? Mein Code funktioniert und dann füge ich eine Methode hinzu, und mein Arbeitscode wird überhaupt nicht kompiliert.

Narek
quelle
3
In C ++ gibt es ohne zusätzlichen Aufwand wie die Verwendung der PIMPL-Sprache keinen echten "privaten" Teil der Klasse. Dies ist nur eines der Probleme (das Hinzufügen einer "privaten" Methodenüberladung und das Unterbrechen des Kompilierens von altem Code zählt in meinem Buch als Problem, auch wenn dieses trivial zu vermeiden ist, indem man es einfach nicht tut), das dadurch verursacht wird.
Hyde
Gibt es einen realen Code, bei dem Sie erwarten würden, eine const-Funktion aufrufen zu können, dessen nicht-const-Gegenstück jedoch Teil der privaten Schnittstelle wäre? Das klingt für mich nach schlechtem Interface-Design.
Vincent Fourmond

Antworten:

125

Wenn Sie aufrufen a.foo();, durchläuft der Compiler die Überlastungsauflösung, um die beste zu verwendende Funktion zu finden. Wenn es die Überlastmenge erstellt, findet es

void foo() const

und

void foo()

Da dies anicht constder Fall ist , ist die nicht konstante Version die beste Übereinstimmung, daher wählt der Compiler void foo(). Dann werden die Zugriffsbeschränkungen eingeführt und Sie erhalten einen Compilerfehler, da dieser void foo()privat ist.

Denken Sie daran, dass es bei der Überlastungsauflösung nicht darum geht, die beste verwendbare Funktion zu finden. Es ist 'finde die beste Funktion und versuche sie zu benutzen'. Wenn dies aufgrund von Zugriffsbeschränkungen oder Löschen nicht möglich ist, wird ein Compilerfehler angezeigt.

Mit anderen Worten, warum kommt die Überlastungsauflösung vor der Zugriffskontrolle?

Schauen wir uns an:

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);
}

Nehmen wir jetzt an, ich wollte eigentlich nicht void foo(Derived * d)privat machen . Wenn die Zugriffskontrolle an erster Stelle steht, wird dieses Programm kompiliert und ausgeführt und Basegedruckt. Dies kann in einer großen Codebasis sehr schwer zu finden sein. Da die Zugriffskontrolle nach der Überlastungsauflösung erfolgt, wird ein netter Compilerfehler angezeigt, der mir mitteilt, dass die Funktion, die aufgerufen werden soll, nicht aufgerufen werden kann, und ich kann den Fehler viel einfacher finden.

NathanOliver
quelle
Gibt es einen Grund, warum die Zugriffskontrolle nach der Überlastungsauflösung erfolgt?
Drake7707
3
@ drake7707 Wie ich in meinem Codebeispiel zeige, würde der obige Code kompiliert, wenn die Zugriffssteuerung zuerst kam, was die Semantik des Programms ändert. Ich bin mir nicht sicher, aber ich hätte lieber einen Fehler und müsste eine explizite Umwandlung durchführen, wenn ich wollte, dass die Funktion privat bleibt, dann eine implizite Umwandlung und der Code "funktioniert" stillschweigend.
NathanOliver
"und müssen eine explizite Besetzung machen, wenn ich wollte, dass die Funktion privat bleibt" - es klingt so, als ob das eigentliche Problem hier implizite Besetzungen sind ... obwohl andererseits die Idee, dass Sie eine abgeleitete Klasse implizit als die verwenden können Basisklasse ist ein bestimmendes Merkmal des OO-Paradigmas, nicht wahr?
Steven Byks
35

Letztendlich ist dies auf die Behauptung im Standard zurückzuführen, dass die Barrierefreiheit bei der Durchführung der Überlastungsauflösung nicht berücksichtigt werden sollte . Diese Behauptung kann in [over.match] Abschnitt 3 gefunden werden:

... Wenn die Überlastungsauflösung erfolgreich ist und die bestmögliche Funktion in dem Kontext, in dem sie verwendet wird, nicht verfügbar ist (Klausel [class.access]), ist das Programm fehlerhaft.

und auch die Anmerkung in Abschnitt 1 desselben Abschnitts:

[Hinweis: Es wird nicht garantiert, dass die durch Überlastungsauflösung ausgewählte Funktion für den Kontext geeignet ist. Andere Einschränkungen, wie die Zugänglichkeit der Funktion, können ihre Verwendung im aufrufenden Kontext schlecht formulieren. - Endnote]

Was den Grund betrifft, kann ich mir einige mögliche Motivationen vorstellen:

  1. Es verhindert unerwartete Verhaltensänderungen infolge der Änderung der Zugänglichkeit eines Überlastungskandidaten (stattdessen tritt ein Kompilierungsfehler auf).
  2. Es entfernt die Kontextabhängigkeit aus dem Überlastungsauflösungsprozess (dh die Überlastungsauflösung würde innerhalb oder außerhalb der Klasse das gleiche Ergebnis haben).
Atkins
quelle
32

Angenommen, die Zugriffskontrolle erfolgte vor der Überlastungsauflösung. Tatsächlich würde dies bedeuten, dass public/protected/privatedie Sichtbarkeit und nicht die Zugänglichkeit kontrolliert wird.

Abschnitt 2.10 von Design und Evolution von C ++ von Stroustrup enthält eine Passage, in der er das folgende Beispiel erläutert

int a; // global a

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

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

Stroustrup erwähnt , dass ein Vorteil der geltenden Regeln (Sichtbarkeit vor Zugänglichkeit) , die (vorübergehend) die chaning privateinnen class Xin public(zB für die Zwecke des Debugging) ist , dass es im Sinne des oben genannten Programms keine ruhige Änderung (dh X::awird versucht, in beiden Fällen zugegriffen werden, was im obigen Beispiel einen Zugriffsfehler ergibt). Wenn public/protected/privatedie Sichtbarkeit gesteuert würde, würde sich die Bedeutung des Programms ändern ( andernfalls würde global aaufgerufen ).privateX::a

Er gibt dann an, dass er sich nicht daran erinnert, ob es sich um ein explizites Design oder einen Nebeneffekt der Präprozessortechnologie handelte, die zur Implementierung des C mit Classess-Vorgänger von Standard C ++ verwendet wurde.

Wie hängt das mit Ihrem Beispiel zusammen? Grundsätzlich, weil die vom Standard vorgenommene Überlastungsauflösung der allgemeinen Regel entspricht, dass die Namenssuche vor der Zugriffskontrolle erfolgt.

10.2 Suche nach Mitgliedsnamen [class.member.lookup]

1 Die Suche nach Mitgliedsnamen bestimmt die Bedeutung eines Namens (ID-Ausdrucks) in einem Klassenbereich (3.3.7). Die Namenssuche kann zu Mehrdeutigkeiten führen. In diesem Fall ist das Programm fehlerhaft. Bei einem ID-Ausdruck beginnt die Namenssuche im Klassenbereich. Bei einer qualifizierten ID beginnt die Namenssuche im Bereich des verschachtelten Namensbezeichners. Die Namenssuche erfolgt vor der Zugriffskontrolle (3.4, Abschnitt 11).

8 Wird der Name einer überladenen Funktion eindeutig gefunden, erfolgt die Überladungsauflösung (13.3) auch vor der Zugriffskontrolle . Mehrdeutigkeiten können häufig behoben werden, indem ein Name mit seinem Klassennamen qualifiziert wird.

TemplateRex
quelle
23

Da der implizite thisZeiger nicht ist const, prüft der Compiler constvor einer constVersion zunächst, ob eine Nichtversion der Funktion vorhanden ist .

Wenn Sie sich ausdrücklich nicht markieren constein , privatedann wird die Auflösung fehl und der Compiler wird nicht weitersuchen.

Bathseba
quelle
Denken Sie, dass es konsequent ist? Mein Code funktioniert und dann füge ich eine Methode hinzu und mein Arbeitscode wird überhaupt nicht kompiliert.
Narek
Ich glaube schon. Überlastungsauflösung ist absichtlich pingelig. Ich habe gestern eine ähnliche Frage beantwortet: stackoverflow.com/questions/39023325/…
Bathsheba
5
@ Narek Ich glaube, es funktioniert genauso wie gelöschte Funktionen bei der Überlastungsauflösung. Es wählt das beste aus dem Set aus und sieht dann, dass es nicht verfügbar ist, sodass Sie einen Compilerfehler erhalten. Es wählt nicht die beste verwendbare Funktion, sondern die beste Funktion und versucht dann, sie zu verwenden.
NathanOliver
3
@Narek Ich habe mich auch zuerst gefragt, warum es nicht funktioniert, aber bedenken Sie Folgendes: Wie würden Sie jemals die private Funktion aufrufen, wenn die öffentliche Konstante auch für Nicht-Konstantenobjekte ausgewählt werden sollte?
idclev 463035818
20

Es ist wichtig, die Reihenfolge der Ereignisse zu berücksichtigen:

  1. Finden Sie alle realisierbaren Funktionen.
  2. Wählen Sie die bestmögliche Funktion.
  3. Wenn es nicht genau eine brauchbare Funktion gibt oder wenn Sie die bestmögliche Funktion nicht aufrufen können (aufgrund von Zugriffsverletzungen oder der Funktion deleted), schlagen Sie fehl.

(3) geschieht nach (2). Was wirklich wichtig ist, weil sonst das Erstellen von Funktionen deleted oder privatebedeutungslos und viel schwieriger zu überlegen wäre.

In diesem Fall:

  1. Die realisierbaren Funktionen sind A::foo()und A::foo() const.
  2. Die am besten realisierbare Funktion besteht darin, A::foo()dass letztere eine Qualifizierungskonvertierung für das implizite thisArgument beinhaltet.
  3. Aber A::foo()ist privateund Sie haben keinen Zugriff darauf, daher ist der Code schlecht geformt.
Barry
quelle
1
Man könnte denken, "tragfähig" würde relevante Zugangsbeschränkungen beinhalten. Mit anderen Worten, es ist nicht "praktikabel", eine private Funktion von außerhalb der Klasse aufzurufen, da sie nicht Teil der öffentlichen Schnittstelle dieser Klasse ist.
RM
15

Dies hängt von einer ziemlich grundlegenden Entwurfsentscheidung in C ++ ab.

Wenn der Compiler nach der Funktion sucht, um einen Aufruf zu erfüllen, führt er eine Suche wie folgt durch:

  1. Es wird nach dem ersten finden 1 Umfang , an das es etwas mit diesem Namen.

  2. Der Compiler findet alle Funktionen (oder Funktoren usw.) mit diesem Namen in diesem Bereich.

  3. Anschließend führt der Compiler eine Überlastungsauflösung durch, um den besten Kandidaten unter den gefundenen zu finden (unabhängig davon, ob auf sie zugegriffen werden kann oder nicht).

  4. Schließlich prüft der Compiler, ob auf diese ausgewählte Funktion zugegriffen werden kann.

Aufgrund dieser Reihenfolge ist es möglich, dass der Compiler eine Überladung auswählt, auf die nicht zugegriffen werden kann, obwohl eine andere Überladung verfügbar ist (die jedoch während der Überlastungsauflösung nicht ausgewählt wird).

Ob es möglich wäre, Dinge anders zu machen: Ja, das ist zweifellos möglich. Es würde definitiv zu einer ganz anderen Sprache als C ++ führen. Es stellt sich heraus, dass viele scheinbar eher geringfügige Entscheidungen Auswirkungen haben können, die viel mehr betreffen, als zunächst offensichtlich sein könnte.


  1. "First" kann an sich etwas komplex sein, insbesondere wenn / wenn Vorlagen involviert sind, da sie zu einer zweiphasigen Suche führen können, was bedeutet, dass es zwei völlig separate "Roots" gibt, von denen aus bei der Suche begonnen werden kann. Die Grundidee ist jedoch ziemlich einfach: Beginnen Sie mit dem kleinsten umschließenden Bereich und arbeiten Sie sich nach außen zu immer größeren umschließenden Bereichen vor.
Jerry Sarg
quelle
1
Stroustrup spekuliert in D & E, dass die Regel ein Nebeneffekt des in C verwendeten Präprozessors mit Klassen sein könnte, die nie überprüft wurden, sobald die fortschrittlichere Compilertechnologie verfügbar wurde. Siehe meine Antwort .
TemplateRex
12

Zugangskontrollen ( public, protected, private) haben keinen Einfluss auf die Überladungsauflösung. Der Compiler wählt, void foo()weil es die beste Übereinstimmung ist. Die Tatsache, dass es nicht zugänglich ist, ändert daran nichts. Wenn Sie es entfernen, bleibt nur void foo() constdas übrig , was dann die beste (dh einzige) Übereinstimmung ist.

Pete Becker
quelle
11

In diesem Aufruf:

a.foo();

thisIn jeder Mitgliedsfunktion ist immer ein impliziter Zeiger verfügbar. Und die constQualifikation von thiswird der aufrufenden Referenz / dem aufrufenden Objekt entnommen. Der obige Aufruf behandelt wird vom Compiler als:

A::foo(a);

Sie haben jedoch zwei Erklärungen, A::foodie wie folgt behandelt werden :

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

Durch die Überlastungsauflösung wird die erste für Nicht-Konstante ausgewählt this, die zweite für a const this. Wenn Sie die erste entfernen, wird die zweite an beide constund gebunden non-const this.

Nach der Überlastungsauflösung zur Auswahl der bestmöglichen Funktion erfolgt die Zugriffskontrolle. Da Sie den Zugriff auf die ausgewählte Überladung als angegeben haben private, beschwert sich der Compiler.

Der Standard sagt es so:

[class.access / 4] : ... Bei überladenen Funktionsnamen wird die Zugriffssteuerung auf die durch Überlastungsauflösung ausgewählte Funktion angewendet ....

Aber wenn Sie dies tun:

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

Dann wird nur die constÜberlast angepasst.

WhiZTiM
quelle
Das ist seltsam, dass nach der Überlastungsauflösung zur Auswahl der bestmöglichen Funktion die Zugriffskontrolle erfolgt . Die Zugriffskontrolle sollte vor der Überlastungsauflösung erfolgen, als ob Sie keinen Zugriff hätten, sollten Sie dies überhaupt nicht berücksichtigen. Was denken Sie?
Narek
@ Narek, .. Ich habe meine Antwort mit einem Verweis auf den C ++ - Standard aktualisiert . Auf diese Weise macht es tatsächlich Sinn, dass es in C ++ viele Dinge und Redewendungen gibt, die von diesem Verhalten
abhängen
9

Der technische Grund wurde durch andere Antworten beantwortet. Ich werde mich nur auf diese Frage konzentrieren:

Mit anderen Worten, warum kommt die Überlastungsauflösung vor der Zugriffskontrolle? Das ist merkwürdig. Denken Sie, dass es konsequent ist? Mein Code funktioniert und dann füge ich eine Methode hinzu und mein Arbeitscode wird überhaupt nicht kompiliert.

So wurde die Sprache gestaltet. Die Absicht ist es, so weit wie möglich die bestmögliche Überlastung zu nennen. Wenn dies fehlschlägt, wird ein Fehler ausgelöst, der Sie daran erinnert, das Design erneut zu prüfen.

Angenommen, Ihr Code wurde kompiliert und funktioniert gut mit der constaufgerufenen Member-Funktion. Eines Tages beschließt dann jemand (vielleicht Sie selbst), die Zugänglichkeit der Nichtmitgliedsfunktion constvon privateauf zu ändern public. Dann würde sich das Verhalten ohne Kompilierungsfehler ändern! Das wäre eine Überraschung .

songyuanyao
quelle
8

Weil die Variable ain der mainFunktion nicht als deklariert ist const.

Konstante Elementfunktionen werden für konstante Objekte aufgerufen.

Ein Programmierer
quelle
8

Zugriffsspezifizierer haben niemals Einfluss auf die Namenssuche und die Auflösung von Funktionsaufrufen. Die Funktion wird ausgewählt, bevor der Compiler prüft, ob der Aufruf eine Zugriffsverletzung auslösen soll.

Auf diese Weise werden Sie beim Ändern eines Zugriffsspezifizierers beim Kompilieren benachrichtigt, wenn ein Verstoß gegen den vorhandenen Code vorliegt. Wenn die Privatsphäre bei der Auflösung von Funktionsaufrufen berücksichtigt würde, könnte sich das Verhalten Ihres Programms stillschweigend ändern.

Kyle Strand
quelle