Dlaczego metoda stała publiczna nie jest wywoływana, skoro metoda inna niż stała jest prywatna?

117

Rozważ ten kod:

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

Błąd kompilatora to:

błąd: „void A :: foo ()” jest prywatne ”.

Ale kiedy usuwam prywatny, po prostu działa. Dlaczego metoda public const nie jest wywoływana, gdy metoda inna niż stała jest prywatna?

Innymi słowy, dlaczego rozwiązanie przeciążenia następuje przed kontrolą dostępu? To jest dziwne. Czy uważasz, że jest to spójne? Mój kod działa, a następnie dodaję metodę, a mój działający kod w ogóle się nie kompiluje.

Narek
źródło
3
W C ++, bez dodatkowego wysiłku, takiego jak użycie idiomu PIMPL, nie ma prawdziwej „prywatnej” części klasy. To tylko jeden z problemów (dodanie przeciążenia metody „prywatnej” i zerwanie kompilacji starego kodu liczy się jako problem w mojej książce, nawet jeśli jest to trywialne, aby go uniknąć, po prostu nie robiąc tego).
hyde
Czy istnieje prawdziwy kod, w którym można by się spodziewać wywołania funkcji const, ale jej odpowiednik inny niż const będzie częścią prywatnego interfejsu? Brzmi to jak zły projekt interfejsu.
Vincent Fourmond,

Odpowiedzi:

125

Podczas wywołania a.foo();kompilator przechodzi przez rozwiązanie przeciążenia, aby znaleźć najlepszą funkcję do użycia. Podczas kompilowania znalezionego zestawu przeciążeń

void foo() const

i

void foo()

Ponieważ atak nie jest const, wersja inna niż stała jest najlepszym dopasowaniem, więc kompilator wybiera void foo(). Następnie wprowadzane są ograniczenia dostępu i pojawia się błąd kompilatora, ponieważ void foo()jest prywatny.

Pamiętaj, że w rozwiązaniu problemu z przeciążeniem nie chodzi o „znalezienie najlepszej użytecznej funkcji”. To „znajdź najlepszą funkcję i spróbuj jej użyć”. Jeśli nie może z powodu ograniczeń dostępu lub usunięcia, pojawia się błąd kompilatora.

Innymi słowy, dlaczego rozwiązanie przeciążenia następuje przed kontrolą dostępu?

Cóż, spójrzmy na:

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

Powiedzmy teraz, że tak naprawdę nie chciałem robić void foo(Derived * d)czegoś prywatnego. Gdyby kontrola dostępu pojawiła się jako pierwsza, program ten skompilowałby się, uruchomiłby i Basezostałby wydrukowany. Może to być bardzo trudne do wyśledzenia w dużej bazie kodu. Ponieważ kontrola dostępu następuje po rozwiązaniu problemu z przeciążeniem, pojawia się miły błąd kompilatora, który mówi mi, że funkcja, którą chcę, aby wywołać, nie może zostać wywołana, a błąd jest znacznie łatwiejszy.

NathanOliver
źródło
Czy jest jakiś powód, dla którego kontrola dostępu jest po rozwiązaniu problemu z przeciążeniem?
drake7707
3
@ drake7707 Wll, jak pokazuję w moim przykładzie kodu, jeśli kontrola dostępu byłaby pierwsza, wówczas powyższy kod zostałby skompilowany, co zmienia semantykę programu. Nie jestem pewien co do ciebie, ale wolałbym mieć błąd i potrzebować jawnego rzutowania, gdybym chciał, aby funkcja pozostała prywatna, a następnie niejawne rzutowanie i kod cicho „działa”.
NathanOliver
„i muszę wykonać wyraźne rzutowanie, jeśli chcę, aby funkcja pozostała prywatna” - wydaje się, że prawdziwym problemem są tutaj niejawne rzutowania ... chociaż z drugiej strony pomysł, że można również użyć klasy pochodnej niejawnie jako klasa bazowa jest charakterystyczną cechą paradygmatu OO, prawda?
Steven Byks
35

Ostatecznie sprowadza się to do stwierdzenia w standardzie, że dostępność nie powinna być brana pod uwagę podczas rozwiązywania problemu z przeciążeniem . To stwierdzenie można znaleźć w [over.match] klauzuli 3:

... Kiedy rozwiązanie przeciążenia powiedzie się, a najlepsza funkcjonalna funkcja nie jest dostępna (klauzula [class.access]) w kontekście, w którym jest używana, program jest źle sformułowany.

a także uwaga w punkcie 1 tej samej sekcji:

[Uwaga: Nie ma gwarancji, że funkcja wybrana przez rozwiązanie przeciążenia będzie odpowiednia dla kontekstu. Inne ograniczenia, takie jak dostępność funkcji, mogą spowodować, że jej użycie w kontekście wywołania będzie źle sformułowane. - notatka końcowa]

Jeśli chodzi o przyczyny, przychodzi mi do głowy kilka możliwych motywacji:

  1. Zapobiega nieoczekiwanym zmianom zachowania w wyniku zmiany dostępności kandydata na przeciążenie (zamiast tego wystąpi błąd kompilacji).
  2. Usuwa zależność od kontekstu z procesu rozpoznawania przeciążenia (tj. Rozpoznanie przeciążenia miałoby ten sam wynik w klasie, czy poza nią).
Atkins
źródło
32

Załóżmy, że kontrola dostępu nastąpiła przed rozwiązaniem problemu przeciążenia. W rzeczywistości oznaczałoby to public/protected/privatekontrolowaną widoczność, a nie dostępność.

Sekcja 2.10 projektu i ewolucji C ++ autorstwa Stroustrupa zawiera fragment na ten temat, w którym omawia następujący przykład

int a; // global a

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

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

Stroustrup wspomina, że korzyści wynikające z obowiązujących przepisów (widoczności przed dostępność) jest to, że (tymczasowo) chaning z privatewnętrza class Xdo public(na przykład dla celów debugowania) to, że nie ma żadnych zmian cicho w rozumieniu powyższego programu (czyli X::apróbuje się być dostępne w obu przypadkach, co daje błąd dostępu w powyższym przykładzie). Gdyby public/protected/privatekontrolował widoczność, znaczenie programu by się zmieniło (w przeciwnym razie global azostałby wywołany with ).privateX::a

Następnie stwierdza, że ​​nie przypomina sobie, czy było to spowodowane jawnym projektem, czy efektem ubocznym technologii preprocesora używanej do implementacji C z poprzednikiem Classess standardowego C ++.

Jak to się ma do twojego przykładu? Zasadniczo dlatego, że standardowe rozpoznawanie przeciążenia jest zgodne z ogólną zasadą, że wyszukiwanie nazw ma miejsce przed kontrolą dostępu.

10.2 Wyszukiwanie nazw członków [class.member.lookup]

1 Wyszukiwanie nazw składowych określa znaczenie nazwy (wyrażenie id) w zakresie klasy (3.3.7). Wyszukiwanie nazwy może skutkować niejednoznacznością, w którym to przypadku program jest źle sformułowany. W przypadku wyrażenia id, wyszukiwanie nazw zaczyna się w zakresie klasy this; w przypadku kwalifikowanego identyfikatora wyszukiwanie nazwy zaczyna się w zakresie zagnieżdżonego specyfikatora nazwy. Wyszukiwanie nazwy ma miejsce przed kontrolą dostępu (3.4, Klauzula 11).

8 Jeśli nazwa przeciążonej funkcji zostanie znaleziona jednoznacznie, to przed kontrolą dostępu następuje również rozwiązanie przeciążenia (13.3) . Niejednoznaczności często można rozwiązać, kwalifikując nazwę za pomocą nazwy klasy.

TemplateRex
źródło
23

Ponieważ niejawny thiswskaźnik jest constinny niż -, kompilator najpierw sprawdzi obecność nie- constwersji funkcji przed constwersją.

Jeśli wyraźnie zaznaczyć nieprzestrzegania constjednego privatenastępnie uchwała nie powiedzie się, a kompilator nie będzie kontynuować poszukiwania.

Batszeba
źródło
Czy uważasz, że jest to spójne? Mój kod działa, a następnie dodaję metodę, a mój kod roboczy w ogóle się nie kompiluje.
Narek
Tak myślę. Rozdzielczość przeciążenia jest celowo wybredna. Odpowiedziałem wczoraj na podobne pytanie: stackoverflow.com/questions/39023325/…
Bathsheba
5
@Narek Uważam, że działa to tak samo, jak działają usunięte funkcje w rozwiązaniu problemu z przeciążeniem. Wybiera najlepszy z zestawu, a następnie widzi, że nie jest dostępny, więc pojawia się błąd kompilatora. Nie wybiera najlepszej użytecznej funkcji, ale najlepszą funkcję, a następnie próbuje jej użyć.
NathanOliver
3
@Narek Najpierw też się zastanawiałem, dlaczego to nie działa, ale zastanów się: jak nazwałbyś funkcję prywatną, gdyby stała publiczna miała być wybrana również dla obiektów innych niż const?
idclev 463035818
20

Ważne jest, aby pamiętać o kolejności rzeczy, które się dzieją, czyli:

  1. Znajdź wszystkie możliwe funkcje.
  2. Wybierz najlepszą wykonalną funkcję.
  3. Jeśli nie ma dokładnie jednej z najlepszych wykonalnych funkcji lub jeśli faktycznie nie można wywołać najlepszej wykonalnej funkcji (z powodu naruszenia dostępu lub funkcji będącej deleted), zakończy się niepowodzeniem.

(3) dzieje się po (2). Co jest naprawdę ważne, ponieważ w przeciwnym razie tworzenie funkcji deleted lub privatestałoby się w pewnym sensie bez znaczenia i znacznie trudniejsze do rozważenia.

W tym przypadku:

  1. Realne funkcje to A::foo()i A::foo() const.
  2. Najlepszą realną funkcją jest to, A::foo()że ta ostatnia obejmuje konwersję kwalifikacji na niejawnym thisargumencie.
  3. Ale A::foo()jest privatei nie masz do niego dostępu, stąd kod jest źle sformułowany.
Barry
źródło
1
Można by pomyśleć, że „wykonalne” obejmowałoby odpowiednie ograniczenia dostępu. Innymi słowy, nie jest "wykonalne" wywołanie funkcji prywatnej spoza klasy, ponieważ nie jest to część publicznego interfejsu tej klasy.
RM
15

Sprowadza się to do dość podstawowej decyzji projektowej w C ++.

Podczas wyszukiwania funkcji w celu spełnienia wywołania kompilator przeprowadza takie wyszukiwanie:

  1. Przeszukuje znaleźć pierwsze 1 zakres, w którym istnieje coś o tej nazwie.

  2. Kompilator znajduje wszystkie funkcje (lub funktory itp.) O tej nazwie w tym zakresie.

  3. Następnie kompilator rozwiązuje przeciążenie, aby znaleźć najlepszego kandydata spośród znalezionych (niezależnie od tego, czy są dostępne, czy nie).

  4. Na koniec kompilator sprawdza, czy wybrana funkcja jest dostępna.

Z powodu tej kolejności tak, możliwe jest, że kompilator wybierze przeciążenie, które jest niedostępne, mimo że istnieje inne przeciążenie, które jest dostępne (ale nie zostało wybrane podczas rozwiązywania przeciążenia).

Czy byłoby możliwe , aby robić rzeczy inaczej: tak, to niewątpliwie możliwe. Z pewnością doprowadziłoby to jednak do całkiem innego języka niż C ++. Okazuje się, że wiele pozornie niewielkich decyzji może mieć konsekwencje, które mają znacznie większy wpływ, niż mogłoby się początkowo wydawać oczywiste.


  1. „Pierwszy” może być sam w sobie trochę skomplikowany, zwłaszcza gdy w grę wchodzą szablony, ponieważ mogą prowadzić do wyszukiwania dwufazowego, co oznacza, że ​​podczas wyszukiwania są dwa całkowicie oddzielne „korzenie”. Podstawowa idea jest dość prosta, choć: zacznij od najmniejszego zakresu otaczającego, i swój sposób pracy na zewnątrz, do większych i większych zakresów obejmujących.
Jerry Coffin
źródło
1
Stroustrup spekuluje w D&E, że reguła może być efektem ubocznym preprocesora używanego w C z klasami, które nigdy nie zostały sprawdzone, gdy pojawiła się bardziej zaawansowana technologia kompilatora. Zobacz moją odpowiedź .
TemplateRex,
12

Kontrola dostępu ( public, protected, private) nie wpływają na przeciążenie rozdzielczość. Kompilator wybiera, void foo()ponieważ jest to najlepsze dopasowanie. Fakt, że jest niedostępny, tego nie zmienia. Usunięcie go pozostawia tylko void foo() const, co jest wtedy najlepszym (tj. Jedynym) dopasowaniem.

Pete Becker
źródło
11

W tym wezwaniu:

a.foo();

W każdej funkcji składowej zawsze dostępny jest niejawny thiswskaźnik. A constkwalifikacja thispochodzi z wywołującego odniesienia / obiektu. Powyższe wywołanie jest traktowane przez kompilator jako:

A::foo(a);

Ale masz dwie deklaracje, A::fooktóre są traktowane jak :

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

Dzięki rozwiązaniu przeciążenia pierwsza zostanie wybrana jako inna niż stała this, a druga zostanie wybrana dla const this. Jeśli usuniesz pierwszy, drugi połączy się z obydwoma consti non-const this.

Po rozwiązaniu problemu z przeciążeniem, aby wybrać najlepszą wykonalną funkcję, następuje kontrola dostępu. Ponieważ określono dostęp do wybranego przeciążenia jako private, kompilator będzie narzekać.

Norma mówi tak:

[class.access / 4] : ... W przypadku przeciążonych nazw funkcji kontrola dostępu jest stosowana do funkcji wybranej przez rozwiązanie przeciążenia ....

Ale jeśli to zrobisz:

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

Wtedy constpasuje tylko przeciążenie.

WhiZTiM
źródło
To jest DZIWNE, że Po rozwiązaniu problemu z przeciążeniem, aby wybrać najlepszą wykonalną funkcję, przychodzi kontrola dostępu . Kontrola dostępu powinna poprzedzać rozwiązanie problemu przeciążenia, tak jakbyś nie miał dostępu, czy w ogóle nie powinieneś tego rozważać, co myślisz?
Narek
@Narek, .. Zaktualizowałem swoją odpowiedź o odniesienie do standardu C ++. W rzeczywistości ma to sens, ponieważ w C ++ jest wiele rzeczy i idiomów, które zależą od tego zachowania
WhiZTiM
9

Na powód techniczny odpowiedziały inne odpowiedzi. Skoncentruję się tylko na tym pytaniu:

Innymi słowy, dlaczego rozwiązanie przeciążenia następuje przed kontrolą dostępu? To jest dziwne. Czy uważasz, że jest to spójne? Mój kod działa, a następnie dodaję metodę, a mój kod roboczy w ogóle się nie kompiluje.

Tak zaprojektowano język. Celem jest próba wywołania najlepszego możliwego przeciążenia, tak dalece, jak to możliwe. Jeśli to się nie powiedzie, zostanie wyzwolony błąd, aby przypomnieć o ponownym rozważeniu projektu.

Z drugiej strony załóżmy, że Twój kod został skompilowany i działał dobrze z constwywoływaną funkcją składową. Któregoś dnia ktoś (może Ty) zdecyduje się zmienić dostępność funkcji niebędącej constczłonkiem z privatena public. Wtedy zachowanie zmieniłoby się bez błędów kompilacji! To byłaby niespodzianka .

songyuanyao
źródło
8

Ponieważ zmienna aw mainfunkcji nie jest zadeklarowana jako const.

Stałe funkcje składowe są wywoływane na stałych obiektach.

Jakiś koleś programista
źródło
8

Specyfikatory dostępu nigdy nie wpływają na wyszukiwanie nazw i rozpoznawanie wywołań funkcji. Funkcja jest wybierana zanim kompilator sprawdzi, czy wywołanie powinno wywołać naruszenie dostępu.

W ten sposób, jeśli zmienisz specyfikator dostępu, zostaniesz ostrzeżony w czasie kompilacji, jeśli nastąpi naruszenie w istniejącym kodzie; Gdyby wziąć pod uwagę prywatność przy rozwiązywaniu wywołań funkcji, zachowanie programu mogłoby się po cichu zmienić.

Kyle Strand
źródło