Pourquoi une méthode publique const n'est-elle pas appelée lorsque la méthode non const est privée?

117

Considérez ce 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();
}

L'erreur du compilateur est:

erreur: 'void A :: foo ()' est privé`.

Mais quand je supprime le privé, cela fonctionne. Pourquoi la méthode public const n'est-elle pas appelée lorsque la méthode non-const est privée?

En d'autres termes, pourquoi la résolution des surcharges passe-t-elle avant le contrôle d'accès? Cela est étrange. Pensez-vous que c'est cohérent? Mon code fonctionne, puis j'ajoute une méthode, et mon code de travail ne se compile pas du tout.

Narek
la source
3
En C ++, sans effort supplémentaire comme l'utilisation de l'idiome PIMPL, il n'y a pas de véritable partie "privée" de la classe. C'est juste l'un des problèmes (l'ajout d'une surcharge de méthode "privée" et la rupture de l'ancien code de compilation compte comme un problème dans mon livre, même si celui-ci est trivial à éviter en ne le faisant pas) causé par cela.
hyde
Existe-t-il un code réel dans lequel vous vous attendriez à pouvoir appeler une fonction const mais que son homologue non const ferait partie de l'interface privée? Cela me semble être une mauvaise conception d'interface.
Vincent Fourmond

Réponses:

125

Lorsque vous appelez a.foo();, le compilateur passe par la résolution de surcharge pour trouver la meilleure fonction à utiliser. Lorsqu'il crée l'ensemble de surcharge qu'il trouve

void foo() const

et

void foo()

Maintenant, puisque ce an'est pas le cas const, la version non-const est la meilleure correspondance, donc le compilateur choisit void foo(). Ensuite, les restrictions d'accès sont mises en place et vous obtenez une erreur de compilation, car void foo()est privé.

Souvenez-vous qu'en résolution de surcharge, il ne s'agit pas de «trouver la meilleure fonction utilisable». C'est «trouver la meilleure fonction et essayer de l'utiliser». Si ce n'est pas le cas en raison de restrictions d'accès ou en cours de suppression, vous obtenez une erreur du compilateur.

En d'autres termes, pourquoi la résolution de surcharge vient-elle avant le contrôle d'accès?

Eh bien, regardons:

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

Maintenant, disons que je ne voulais pas vraiment rendre void foo(Derived * d)privé. Si le contrôle d'accès venait en premier, ce programme se compilerait et s'exécuterait et Baseserait imprimé. Cela pourrait être très difficile à retrouver dans une grande base de code. Étant donné que le contrôle d'accès vient après la résolution de la surcharge, j'obtiens une belle erreur de compilation me disant que la fonction que je veux appeler ne peut pas être appelée, et je peux trouver le bogue beaucoup plus facilement.

NathanOliver
la source
Y a-t-il une raison pour laquelle le contrôle d'accès est après la résolution de la surcharge?
drake7707
3
@ drake7707 Wll comme je le montre dans mon exemple de code, si le contrôle d'accès est venu en premier, le code ci-dessus se compilerait, ce qui changerait la sémantique du programme. Je ne sais pas pour vous mais je préférerais avoir une erreur et avoir besoin de faire un cast explicite si je voulais que la fonction reste privée puis un cast implicite et le code "fonctionne" en silence.
NathanOliver
"et j'ai besoin de faire un cast explicite si je voulais que la fonction reste privée" - il semble que le vrai problème ici soit les cast implicites ... bien que d'un autre côté, l'idée que vous pouvez également utiliser une classe dérivée implicitement comme le la classe de base est une caractéristique déterminante du paradigme OO, n'est-ce pas?
Steven Byks
35

En fin de compte, cela revient à l'affirmation de la norme selon laquelle l' accessibilité ne doit pas être prise en compte lors de la résolution de surcharge . Cette assertion peut être trouvée dans la clause 3 [over.match] :

... Lorsque la résolution de surcharge réussit et que la meilleure fonction viable n'est pas accessible (Clause [class.access]) dans le contexte dans lequel elle est utilisée, le programme est mal formé.

et aussi la note dans la clause 1 de la même section:

[Remarque: la fonction sélectionnée par la résolution de surcharge n'est pas garantie d'être appropriée pour le contexte. D'autres restrictions, telles que l'accessibilité de la fonction, peuvent rendre son utilisation dans le contexte d'appel mal formé. - note de fin]

Quant à savoir pourquoi, je peux penser à quelques motivations possibles:

  1. Cela empêche les changements de comportement inattendus résultant de la modification de l'accessibilité d'un candidat de surcharge (à la place, une erreur de compilation se produira).
  2. Il supprime la dépendance au contexte du processus de résolution de surcharge (c'est-à-dire que la résolution de surcharge aurait le même résultat que ce soit à l'intérieur ou à l'extérieur de la classe).
Atkins
la source
32

Supposons que le contrôle d'accès vienne avant la résolution de surcharge. En fait, cela signifierait public/protected/privateune visibilité contrôlée plutôt qu'une accessibilité.

La section 2.10 de Design and Evolution of C ++ par Stroustrup a un passage à ce sujet où il discute de l'exemple suivant

int a; // global a

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

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

Stroustrup mentionne qu'un avantage des règles actuelles (visibilité avant l' accessibilité) est que (temporairement) chaning l' privateintérieur class Xen public(par exemple aux fins de débogage) est qu'il n'y a pas de changement de calme au sens du programme ci - dessus ( X::aon tente de accessible dans les deux cas, ce qui donne une erreur d'accès dans l'exemple ci-dessus). Si public/protected/privatecontrôlerait la visibilité, la signification du programme changerait (global aserait appelé avec private, sinon X::a).

Il déclare ensuite qu'il ne se souvient pas si c'était par une conception explicite ou un effet secondaire de la technologie de préprocesseur utilisée pour implémenter le C avec le prédécesseur Classess du C ++ standard.

Comment cela est-il lié à votre exemple? Fondamentalement, parce que la norme a rendu la résolution de surcharge conforme à la règle générale selon laquelle la recherche de nom vient avant le contrôle d'accès.

10.2 Recherche de nom de membre [class.member.lookup]

1 La recherche de nom de membre détermine la signification d'un nom (id-expression) dans une portée de classe (3.3.7). La recherche de nom peut entraîner une ambiguïté, auquel cas le programme est mal formé. Pour une expression id, la recherche de nom commence dans la portée de classe de this; pour un identifiant qualifié, la recherche de nom commence dans la portée du spécificateur de nom imbriqué. La recherche de nom a lieu avant le contrôle d'accès (3.4, Article 11).

8 Si le nom d'une fonction surchargée est trouvé sans ambiguïté, la résolution de surcharge (13.3) a également lieu avant le contrôle d'accès . Les ambiguïtés peuvent souvent être résolues en qualifiant un nom avec son nom de classe.

TemplateRex
la source
23

Puisque le thispointeur implicite est non- const, le compilateur vérifiera d'abord la présence d'une non- constversion de la fonction avant une constversion.

Si vous marquez explicitement le non- constun, privatela résolution échouera et le compilateur ne poursuivra pas la recherche.

Bathsheba
la source
Pensez-vous que c'est cohérent? Mon code fonctionne puis j'ajoute une méthode et mon code de travail ne se compile pas du tout.
Narek
Je pense. La résolution des surcharges est volontairement difficile. J'ai répondu à une question similaire hier: stackoverflow.com/questions/39023325/…
Bathsheba
5
@Narek Je pense que cela fonctionne exactement comme les fonctions supprimées en résolution de surcharge. Il choisit le meilleur de l'ensemble, puis il voit qu'il n'est pas disponible, donc vous obtenez une erreur de compilation. Il ne choisit pas la meilleure fonction utilisable mais la meilleure et essaie ensuite de l'utiliser.
NathanOliver
3
@Narek Je me suis également demandé pourquoi cela ne fonctionnait pas, mais considérez ceci: comment appelleriez-vous la fonction privée si la fonction publique const devait être choisie également pour les objets non const?
idclev 463035818
20

Il est important de garder à l'esprit l'ordre des choses qui se produisent, à savoir:

  1. Trouvez toutes les fonctions viables.
  2. Choisissez la meilleure fonction viable.
  3. S'il n'y a pas exactement une meilleure viable, ou si vous ne pouvez pas réellement appeler la meilleure fonction viable (en raison de violations d'accès ou de la fonction étant deleted), échouez.

(3) se produit après (2). Ce qui est vraiment important, car sinon, rendre les fonctions deleted ou privatedeviendrait un peu dénué de sens et beaucoup plus difficile à raisonner.

Dans ce cas:

  1. Les fonctions viables sont A::foo()et A::foo() const.
  2. La meilleure fonction viable est A::foo()que cette dernière implique une conversion de qualification sur l' thisargument implicite .
  3. Mais A::foo()c'est privateet vous n'y avez pas accès, donc le code est mal formé.
Barry
la source
1
On pourrait penser que "viable" inclurait les restrictions d'accès pertinentes. En d'autres termes, il n'est pas "viable" d'appeler une fonction privée de l'extérieur de la classe, car elle ne fait pas partie de l'interface publique de cette classe.
RM
15

Cela revient à une décision de conception assez basique en C ++.

Lors de la recherche de la fonction pour satisfaire un appel, le compilateur effectue une recherche comme celle-ci:

  1. Il cherche à trouver la première 1 portée à laquelle il y a quelque chose avec ce nom.

  2. Le compilateur trouve toutes les fonctions (ou foncteurs, etc.) avec ce nom dans cette portée.

  3. Ensuite, le compilateur surcharge la résolution pour trouver le meilleur candidat parmi ceux qu'il a trouvés (qu'ils soient accessibles ou non).

  4. Enfin, le compilateur vérifie si cette fonction choisie est accessible.

En raison de cet ordre, oui, il est possible que le compilateur choisisse une surcharge qui n'est pas accessible, même s'il existe une autre surcharge qui est accessible (mais non choisie lors de la résolution de surcharge).

Quant à savoir s'il serait possible de faire les choses différemment: oui, c'est sans aucun doute possible. Cela conduirait certainement à un langage assez différent de celui du C ++. Il s'avère que beaucoup de décisions en apparence plutôt mineures peuvent avoir des ramifications qui affectent beaucoup plus qu'il ne semble initialement évident.


  1. «First» peut être un peu complexe en lui-même, surtout quand / si des modèles sont impliqués, car ils peuvent conduire à une recherche en deux phases, ce qui signifie qu'il y a deux «racines» entièrement séparées à partir desquelles commencer la recherche. L' idée de base est cependant assez simple: commencez par la plus petite portée englobante et progressez vers l'extérieur vers des étendues englobantes de plus en plus grandes.
Jerry Coffin
la source
1
Stroustrup spécule dans D&E que la règle pourrait être un effet secondaire du préprocesseur utilisé en C avec des classes qui n'ont jamais été examinées une fois que la technologie de compilation avancée est devenue disponible. Voyez ma réponse .
TemplateRex
12

Contrôles d'accès ( public, protected, private) ne touchent pas la surcharge résolution. Le compilateur choisit void foo()parce que c'est la meilleure correspondance. Le fait qu'il ne soit pas accessible n'y change rien. Le supprimer laisse seulement void foo() const, ce qui est alors la meilleure correspondance (c'est-à-dire la seule).

Pete Becker
la source
11

Dans cet appel:

a.foo();

Il y a toujours un thispointeur implicite disponible dans chaque fonction membre. Et la constqualification de thisest tirée de la référence / objet appelant. L'appel ci-dessus est traité par le compilateur comme:

A::foo(a);

Mais vous avez deux déclarations A::foodont on traite comme :

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

Par résolution de surcharge, le premier sera sélectionné pour non-const this, le second sera sélectionné pour a const this. Si vous supprimez le premier, le second se liera à la fois constet non-const this.

Après la résolution de surcharge pour sélectionner la meilleure fonction viable, vient le contrôle d'accès. Puisque vous avez spécifié l'accès à la surcharge choisie comme private, le compilateur se plaindra alors.

La norme le dit:

[class.access / 4] : ... Dans le cas de noms de fonctions surchargés, le contrôle d'accès est appliqué à la fonction sélectionnée par résolution de surcharge ....

Mais si vous faites ceci:

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

Ensuite, seule la constsurcharge sera adaptée.

WhiZTiM
la source
C'est étrange qu'après la résolution de surcharge pour sélectionner la meilleure fonction viable, vient le contrôle d'accès . Le contrôle d'accès doit venir avant la résolution des surcharges, comme si vous ne pouviez pas y accéder, ne devriez-vous pas en tenir compte du tout, qu'en pensez-vous?
Narek
@Narek, .. J'ai mis à jour ma réponse avec une référence au standard C ++. C'est en fait logique de cette façon, il y a beaucoup de choses et d'idiomes en C ++ qui dépendent de ce comportement
WhiZTiM
9

La raison technique a été répondue par d'autres réponses. Je me concentrerai uniquement sur cette question:

En d'autres termes, pourquoi la résolution des surcharges précède le contrôle d'accès? Cela est étrange. Pensez-vous que c'est cohérent? Mon code fonctionne puis j'ajoute une méthode et mon code de travail ne se compile pas du tout.

C'est ainsi que le langage a été conçu. L'intention est d'essayer d'appeler la meilleure surcharge viable, dans la mesure du possible. En cas d'échec, une erreur sera déclenchée pour vous rappeler de revoir la conception.

D'un autre côté, supposons que votre code soit compilé et fonctionne correctement avec la constfonction membre appelée. Un jour, quelqu'un (peut-être vous-même) décide alors de changer l'accessibilité de la constfonction non- membre de privateà public. Ensuite, le comportement changerait sans aucune erreur de compilation! Ce serait une surprise .

songyuanyao
la source
8

Parce que la variable adans la mainfonction n'est pas déclarée comme const.

Les fonctions membres constantes sont appelées sur des objets constants.

Un mec programmeur
la source
8

Les spécificateurs d'accès n'affectent jamais la recherche de nom et la résolution des appels de fonction. La fonction est sélectionnée avant que le compilateur vérifie si l'appel doit déclencher une violation d'accès.

De cette façon, si vous modifiez un spécificateur d'accès, vous serez alerté au moment de la compilation s'il y a une violation dans le code existant; si la confidentialité était prise en compte pour la résolution des appels de fonction, le comportement de votre programme pourrait changer silencieusement.

Kyle Strand
la source