Perché un metodo const pubblico non viene chiamato quando quello non const è privato?


117

Considera questo codice:

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'errore del compilatore è:

errore: 'void A :: foo ()' è privato`.

Ma quando elimino quello privato funziona. Perché il metodo public const non viene chiamato quando quello non-const è privato?

In altre parole, perché la risoluzione del sovraccarico viene prima del controllo degli accessi? Questo è strano. Pensi che sia coerente? Il mio codice funziona e poi aggiungo un metodo e il mio codice funzionante non si compila affatto.


3
In C ++, senza sforzi aggiuntivi come l'uso dell'idioma PIMPL, non esiste una vera parte "privata" della classe. Questo è solo uno dei problemi (l'aggiunta di un sovraccarico del metodo "privato" e l'interruzione del codice vecchio della compilazione conta come un problema nel mio libro, anche se questo è banale da evitare semplicemente non farlo) causato da esso.
hyde

Esiste un codice reale in cui ci si aspetterebbe di poter chiamare una funzione const ma che la sua controparte non const farebbe parte dell'interfaccia privata? Mi sembra un cattivo design dell'interfaccia.
Vincent Fourmond

Risposte:


125

Quando si chiama a.foo();, il compilatore esegue la risoluzione dell'overload per trovare la funzione migliore da utilizzare. Quando crea il set di sovraccarico che trova

void foo() const

e

void foo()

Ora, poiché anon lo è const, la versione non const è la migliore corrispondenza, quindi il compilatore sceglie void foo(). Quindi vengono messe in atto le restrizioni di accesso e si ottiene un errore del compilatore, poiché void foo()è privato.

Ricorda, nella risoluzione dell'overload non è "trova la migliore funzione utilizzabile". È "trova la funzione migliore e prova a usarla". Se non è possibile a causa delle restrizioni di accesso o dell'eliminazione, viene visualizzato un errore del compilatore.

In altre parole, perché la risoluzione del sovraccarico viene prima del controllo degli accessi?

Bene, diamo un'occhiata a:

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

Ora diciamo che in realtà non intendevo rendere void foo(Derived * d)privato. Se il controllo degli accessi venisse prima, questo programma verrebbe compilato ed eseguito e Baseverrebbe stampato. Questo potrebbe essere molto difficile da rintracciare in una grande base di codice. Poiché il controllo degli accessi arriva dopo la risoluzione del sovraccarico, ricevo un simpatico errore del compilatore che mi dice che la funzione che voglio che chiami non può essere chiamata e posso trovare il bug molto più facilmente.


C'è qualche motivo per cui il controllo degli accessi è dopo la risoluzione del sovraccarico?
drake7707

3
@ drake7707 Come mostro nel mio esempio di codice, se il controllo degli accessi è arrivato per primo, il codice sopra verrebbe compilato, il che cambia la semantica del programma. Non sono sicuro di te, ma preferirei avere un errore e dover eseguire un cast esplicito se volessi che la funzione rimanga privata, quindi un cast implicito e il codice "funziona" silenziosamente.
NathanOliver

"e ho bisogno di fare un cast esplicito se volevo che la funzione rimanga privata" - sembra che il vero problema qui siano i cast impliciti ... sebbene d'altra parte, l'idea che si possa anche usare una classe derivata implicitamente come la classe base è una caratteristica distintiva del paradigma OO, non è vero?
Steven Byks

35

In definitiva, ciò si riduce all'affermazione nello standard che l' accessibilità non dovrebbe essere presa in considerazione quando si esegue la risoluzione del sovraccarico . Questa affermazione può essere trovata nella clausola 3 [over.match] :

... Quando la risoluzione dell'overload ha successo e la migliore funzione praticabile non è accessibile (clausola [class.access]) nel contesto in cui viene utilizzata, il programma è mal formato.

e anche la Nota nella clausola 1 della stessa sezione:

[Nota: non è garantito che la funzione selezionata dalla risoluzione del sovraccarico sia appropriata per il contesto. Altre restrizioni, come l'accessibilità della funzione, possono rendere mal formato il suo utilizzo nel contesto chiamante. - nota finale]

Per quanto riguarda il motivo, posso pensare a un paio di possibili motivazioni:

  1. Impedisce cambiamenti imprevisti di comportamento come risultato della modifica dell'accessibilità di un candidato al sovraccarico (invece, si verificherà un errore di compilazione).
  2. Rimuove la dipendenza dal contesto dal processo di risoluzione del sovraccarico (cioè la risoluzione del sovraccarico avrebbe lo stesso risultato sia all'interno che all'esterno della classe).

32

Supponiamo che il controllo degli accessi sia stato preceduto dalla risoluzione del sovraccarico. In effetti, ciò significherebbe public/protected/privateuna visibilità controllata piuttosto che l'accessibilità.

La sezione 2.10 di Design and Evolution of C ++ di Stroustrup ha un passaggio su questo in cui discute il seguente esempio

int a; // global a

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

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

Stroustrup afferma che un vantaggio delle regole attuali (visibilità prima dell'accessibilità) è che (temporaneamente) cambiare l' privateinterno class Xin public(ad esempio ai fini del debug) è che non vi è alcun cambiamento silenzioso nel significato del programma di cui sopra (cioè X::asi tenta di accessibile in entrambi i casi, il che dà un errore di accesso nell'esempio precedente). Se public/protected/privatecontrollasse la visibilità, il significato del programma cambierebbe (global averrebbe chiamato con private, altrimenti X::a).

Dichiara quindi di non ricordare se è stato un progetto esplicito o un effetto collaterale della tecnologia del preprocessore utilizzata per implementare il predecessore C with Classess di Standard C ++.

Come è correlato al tuo esempio? Fondamentalmente perché lo standard ha reso la risoluzione dell'overload conforme alla regola generale secondo cui la ricerca del nome viene prima del controllo di accesso.

10.2 Ricerca del nome del membro [class.member.lookup]

1 La ricerca del nome del membro determina il significato di un nome (espressione-id) in un ambito di classe (3.3.7). La ricerca del nome può generare un'ambiguità, nel qual caso il programma è mal formato. Per un'espressione id, la ricerca del nome inizia nell'ambito della classe di this; per un id-qualificato, la ricerca del nome inizia nell'ambito dell'identificatore del nome nidificato. La ricerca del nome avviene prima del controllo dell'accesso (3.4, clausola 11).

8 Se il nome di una funzione sovraccaricata viene trovato in modo univoco, viene eseguita anche la risoluzione del sovraccarico (13.3) prima del controllo dell'accesso . Le ambiguità possono spesso essere risolte qualificando un nome con il nome della sua classe.


23

Poiché il thispuntatore implicito è non- const, il compilatore verificherà prima la presenza di una non constversione della funzione prima di una constversione.

Se si contrassegna esplicitamente il non constuno, privatela risoluzione fallirà e il compilatore non continuerà la ricerca.


Pensi che sia coerente? Il mio codice funziona e poi aggiungo un metodo e il mio codice funzionante non si compila affatto.
Narek

Penso di sì. La risoluzione del sovraccarico è intenzionalmente esigente. Ho risposto ieri a una domanda simile: stackoverflow.com/questions/39023325/…
Bathsheba

5
@ Narek Credo che funzioni proprio come le funzioni eliminate nella risoluzione di sovraccarico. Sceglie il migliore dal set e poi vede che non è disponibile, quindi ottieni un errore del compilatore. Non sceglie la migliore funzione utilizzabile ma la migliore funzione e poi cerca di usarla.
NathanOliver

3
@Narek Per prima cosa mi sono chiesto perché non funziona, ma considera questo: come chiameresti mai la funzione private se quella public const dovesse essere scelta anche per oggetti non const?
idclev 463035818

20

È importante tenere a mente l'ordine delle cose che accadono, ovvero:

  1. Trova tutte le funzioni possibili.
  2. Scegli la migliore funzione praticabile.
  3. Se non esiste esattamente una migliore valida, o se non puoi effettivamente chiamare la migliore funzione valida (a causa di violazioni di accesso o la funzione è deleted), fallisci.

(3) accade dopo (2). Il che è molto importante, perché altrimenti rendere le funzioni deletedo privatediventerebbe in qualche modo privo di significato e molto più difficile da ragionare.

In questo caso:

  1. Le funzioni vitali sono A::foo()e A::foo() const.
  2. La migliore funzione praticabile è A::foo()perché quest'ultima implica una conversione di qualificazione thissull'argomento implicito .
  3. Ma A::foo()è privatee non hai accesso ad esso, quindi il codice è mal formato.

1
Si potrebbe pensare che "fattibile" includa restrizioni di accesso pertinenti. In altre parole, non è "praticabile" chiamare una funzione privata dall'esterno della classe, poiché non fa parte dell'interfaccia pubblica di quella classe.
RM

15

Ciò si riduce a una decisione di progettazione abbastanza semplice in C ++.

Quando si cerca la funzione per soddisfare una chiamata, il compilatore esegue una ricerca come questa:

  1. Si cerca di trovare il primo 1 ambito in cui c'è qualcosa con quel nome.

  2. Il compilatore trova tutte le funzioni (o funtori, ecc.) Con quel nome in tale ambito.

  3. Quindi il compilatore esegue la risoluzione dell'overload per trovare il miglior candidato tra quelli che ha trovato (indipendentemente dal fatto che siano accessibili o meno).

  4. Infine, il compilatore controlla se la funzione scelta è accessibile.

A causa di tale ordine, sì, è possibile che il compilatore scelga un sovraccarico non accessibile, anche se c'è un altro sovraccarico accessibile (ma non scelto durante la risoluzione dell'overload).

Quanto alla possibilità di fare le cose diversamente: sì, è indubbiamente possibile. Tuttavia, porterebbe sicuramente a un linguaggio abbastanza diverso dal C ++. Si scopre che molte decisioni apparentemente piuttosto minori possono avere ramificazioni che influenzano molto di più di quanto potrebbe essere inizialmente ovvio.


  1. "Primo" può essere un po 'complesso di per sé, specialmente quando / se i modelli vengono coinvolti, poiché possono portare a una ricerca in due fasi, il che significa che ci sono due "radici" completamente separate da cui partire quando si esegue la ricerca. L' idea di base è piuttosto semplice: inizia dal più piccolo ambito di recinzione e procedi verso l'esterno verso ambiti di recinzione sempre più grandi.

1
Stroustrup ipotizza in D&E che la regola potrebbe essere un effetto collaterale del preprocessore utilizzato in C con classi che non sono mai state riviste una volta che la tecnologia del compilatore più avanzata è diventata disponibile. Vedi la mia risposta .
TemplateRex

12

I controlli di accesso ( public, protected, private) non influenzano il sovraccarico risoluzione. Il compilatore sceglie void foo()perché è la migliore corrispondenza. Il fatto che non sia accessibile non lo cambia. Rimuovendolo rimane solo void foo() const, che è quindi la migliore (cioè l'unica) corrispondenza.


11

In questa chiamata:

a.foo();

C'è sempre un thispuntatore implicito disponibile in ogni funzione membro. E la constqualifica di thisè presa dal riferimento / oggetto chiamante. La chiamata di cui sopra è trattata dal compilatore come:

A::foo(a);

Ma hai due dichiarazioni di A::foocui viene trattata come :

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

Per risoluzione di sovraccarico, il primo sarà selezionato per non-const this, il secondo sarà selezionato per a const this. Se rimuovi il primo, il secondo si legherà a entrambi conste non-const this.

Dopo la risoluzione del sovraccarico per selezionare la migliore funzione praticabile, arriva il controllo degli accessi. Poiché hai specificato l'accesso all'overload scelto come private, il compilatore si lamenterà.

Lo standard dice così:

[class.access / 4] : ... In caso di nomi di funzione sovraccaricati, il controllo di accesso viene applicato alla funzione selezionata dalla risoluzione del sovraccarico ....

Ma se fai questo:

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

Quindi, solo il constsovraccarico sarà adatto.


Questo è STRANO che dopo la risoluzione del sovraccarico per selezionare la migliore funzione praticabile, arriva il controllo degli accessi . Il controllo degli accessi dovrebbe venire prima della risoluzione del sovraccarico, come se non avessi accesso non dovresti considerarlo affatto, cosa ne pensi?
Narek

@ Narek, .. Ho aggiornato la mia risposta con un riferimento allo standard C ++. In realtà ha senso in questo modo, ci sono molte cose e
espressioni

9

Il motivo tecnico è stato risolto da altre risposte. Mi concentrerò solo su questa domanda:

In altre parole, perché la risoluzione del sovraccarico viene prima del controllo degli accessi? Questo è strano. Pensi che sia coerente? Il mio codice funziona e poi aggiungo un metodo e il mio codice funzionante non si compila affatto.

È così che è stato progettato il linguaggio. L'intento è cercare di chiamare il miglior sovraccarico possibile, per quanto possibile. Se fallisce, verrà attivato un errore per ricordarti di considerare di nuovo il progetto.

D'altra parte, si supponga che il codice sia stato compilato e abbia funzionato bene con la constfunzione membro invocata. Un giorno, qualcuno (forse te stesso) decide quindi di modificare l'accessibilità della constfunzione non membro da privatea public. Quindi, il comportamento cambierebbe senza errori di compilazione! Questa sarebbe una sorpresa .


8

Perché la variabile anella mainfunzione non è dichiarata comeconst .

Le funzioni membro costanti vengono chiamate su oggetti costanti.


8

Gli specificatori di accesso non influenzano mai la ricerca del nome e la risoluzione delle chiamate di funzione. La funzione viene selezionata prima che il compilatore controlli se la chiamata deve attivare una violazione di accesso.

In questo modo, se modifichi uno specificatore di accesso, sarai avvisato in fase di compilazione se c'è una violazione nel codice esistente; se la privacy fosse presa in considerazione per la risoluzione delle chiamate di funzione, il comportamento del programma potrebbe cambiare silenziosamente.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.