Standard C++
- poprawność Const
- co to jest “poprawność const”?
- w jaki sposób “poprawność const” ma związek z bezpieczeństwem zwykłego typu?
- czy powinienem spróbować naprawić wszystko ” wcześniej “czy”później”?
- co oznacza “const X * p”?
- Jaka jest różnica między “const X * p”, “X* const p”i” const X* const p”?
- co oznacza “const X & x”?
- co oznaczają “x const& x” i “X const* p”?
- czy “X& const x” ma jakiś sens?
- co to jest “funkcja const member”?
- jaka jest zależność między funkcją return-by-reference a funkcją const?
- o co chodzi z “const-overloading”?
- jak może mi pomóc zaprojektować lepsze klasy, jeśli odróżnię stan logiczny od stanu fizycznego?
- czy stałość moich funkcji Public member powinna być oparta na tym,co metoda robi ze stanem logicznym obiektu, czy fizycznym?
- co zrobić, jeśli chcę, aby funkcja członka const dokonała “niewidocznej” zmiany na członie danych?
- czy const_cast oznacza utracone możliwości optymalizacji?
- dlaczego kompilator pozwala mi zmienić int po wskazaniu na niego const int*?
- czy “const * P” oznacza, że * p nie może się zmienić?
- dlaczego otrzymuję błąd konwersji Foo** → const Foo**?
poprawność Const
co to jest “poprawność const”?
dobra rzecz. Oznacza to użycie słowa kluczowego const
, aby zapobiec mutacji obiektów const
.
na przykład, jeśli chcesz utworzyć funkcję f()
, która zaakceptowała std::string
, plus chcesz obiecać rozmówcom, że nie zmienią wywołującego std::string
, który zostanie przekazany do f()
, możesz f()
otrzymać jej parametr std::string
…
-
void f1(const std::string& s);
// Pass by reference-to-const
-
void f2(const std::string* sptr);
// podaj wskaźnik-do-const
-
void f3(std::string s);
// wartość Pass by
w przypadkach pass by reference-to – const
I pass by pointer-to – const
, wszelkie próby zmiany wartości wywołującegostd::string
w ramach funkcji f()
byłyby oznaczane przez kompilator jako błąd przy czas kompilacji. Ta kontrola jest wykonywana w całości w czasie kompilacji: nie ma miejsca na czas pracy ani kosztu prędkości dla const
. W przypadku wartości pass by(f3()
), wywołana funkcja otrzymuje kopię wywołującego std::string
. Oznacza to, że f3()
może zmienić swój localcopy, ale kopia jest niszczona, gdy f3()
powraca. W szczególności f3()
nie może zmienić obiektu std::string
wywołującego.
jako przeciwny przykład, załóżmy, że chcesz utworzyć funkcję g()
, która zaakceptowała std::string
, ale chcesz dać znać, że g()
może zmienić obiekt std::string
wywołującego. W tym przypadku możesz otrzymać g()
swój parametrstd::string
…
-
void g1(std::string& s);
// Pass by reference-to-non-const
-
void g2(std::string* sptr);
// przekaż pointer-to-non-const
brak const
w tych funkcjach mówi kompilatorowi, że mogą (ale nie muszą) zmieniać obiekt std::string
. W ten sposób mogą przekazać swoją std::string
do dowolnej funkcji f()
, ale tylko f3()
(ta, która otrzymuje swój parametr “przez wartość”) może przekazać swoją std::string
do g1()
lub g2()
. Jeśli f1()
lub f2()
muszą wywołać funkcję g()
, lokalna kopia obiektu std::string
musi zostać przekazana do funkcji g()
; parametr do f1()
lub f2()
nie może być bezpośrednio przekazany do funkcji g()
. Np..,
void g1(std::string& s);void f1(const std::string& s){ g1(s); // Compile-time Error since s is const std::string localCopy = s; g1(localCopy); // Okay since localCopy is not const}
naturalnie w powyższym przypadku, wszelkie zmiany dokonane przez g1()
są dokonywane w obiekcie localCopy
, który jest lokalny dla f1()
. w szczególności nie zostaną wprowadzone żadne zmiany w parametrze const
, który został przekazany przez odniesienie do f1()
.
w jaki sposób “poprawność const” ma związek z bezpieczeństwem zwykłego typu?
deklarowanie const
-ness parametru jest po prostu kolejną formą bezpieczeństwa typu.
jeśli okaże się, że zwykłe bezpieczeństwo typu pomaga w poprawności systemów (tak jest; zwłaszcza w dużych systemach), znajdzieszconst
poprawność również pomaga.
zaletą poprawności const
jest to, że uniemożliwia ona niezamierzone modyfikowanie czegoś, czego się nie spodziewałeś. Kończy się to koniecznością udekorowania kodu kilkoma dodatkowymi naciśnięciami klawiszy (słowo kluczowe const
), z tym, że mówisz kompilatorowi i innym programistom dodatkowy kawałek ważnej informacji semantycznej— informacji, której kompilator używa, aby zapobiec błędom, a inni programiści używają jako dokumentacji.
koncepcyjnie można sobie wyobrazić, że na przykład const std::string
jest inną klasą niż zwykła std::string
, ponieważ w wariancieconst
koncepcyjnie brakuje różnych operacji mutacyjnych dostępnych w wariancie innym niż const
. Na przykład, możesz koncepcyjnie wyobrazić sobie, że const std::string
po prostu nie ma operatora przypisania+=
ani żadnych innych operacji mutacyjnych.
czy powinienem spróbować naprawić wszystko ” wcześniej “czy”później”?
na samym, bardzo, bardzo początku.
poprawność const
powoduje efekt kuli śnieżnej: każdy const
, kiedy dodasz “tutaj”, wymaga dodania “tam” o cztery więcej.”
Dodaj const
wcześnie i często.
co oznacza “const X * p”?
oznacza to, że p
wskazuje na obiekt klasy X
, ale p
nie może być użyte do zmiany tego obiektu X
(oczywiście p
może również być NULL
).
Czytaj od prawej do lewej: “p jest wskaźnikiem do X, który jest stały .”
na przykład, jeśli Klasa X
ma funkcję członka const
, taką jak inspect() const
, można powiedziećp->inspect()
. Ale jeśli Klasa X
ma funkcję członka innego niżconst
o nazwie mutate()
, jest to błąd, jeśli powiesz p->mutate()
.
co istotne, ten błąd jest wyłapywany przez kompilator w czasie kompilacji-nie są wykonywane testy w czasie wykonywania. Oznacza to, że const
nie spowalnia Twojego programu i nie wymaga pisania dodatkowych przypadków testowych w celu sprawdzenia rzeczy w czasie wykonywania-kompilator wykonuje pracę w czasie kompilacji.
Jaka jest różnica między “const X * p”, “X* const p”i” const X* const p”?
odczytaj deklaracje wskaźnika od prawej do lewej.
-
const X* p
oznacza ”p
wskazuje naX
czyliconst
“: obiektuX
nie można zmienić za pomocąp
. -
X* const p
oznacza “p
jest wskaźnikiemconst
naX
, który nie jestconst
“: nie możesz zmienić wskaźnikap
, ale możesz zmienić obiektX
poprzezp
. -
const X* const p
oznacza “p
jest wskaźnikiemconst
naX
czyliconst
“: nie można zmienić samego wskaźnikap
, ani nie można zmienić obiektuX
poprzezp
.
i o tak, czy wspomniałem o przeczytaniu Twoich deklaracji wskaźnika od prawej do lewej?
co oznacza “const X & x”?
oznacza to, że x
aliasy obiektu X
, ale nie możesz zmienić tego obiektu X
poprzez x
.
Czytaj od prawej do lewej: “x
jest odniesieniem do X
czyli const
.”
na przykład, jeśli Klasa X
ma funkcję członka const
, taką jak inspect() const
, można powiedziećx.inspect()
. Ale jeśli Klasa X
ma funkcję członka innego niżconst
o nazwie mutate()
, jest to błąd, jeśli powiesz x.mutate()
.
jest to całkowicie symetryczne ze wskaźnikami do const, włączając w to fakt, że kompilator wykonuje wszystkie kontrole w czasie kompilacji, co oznacza, że const
nie spowalnia Twojego programu i nie wymaga pisania dodatkowych przypadków testowych, aby sprawdzić rzeczy w czasie wykonywania.
co oznaczają “x const& x” i “X const* p”?
X const& x
jest odpowiednikiem const X& x
, a X const* x
jest odpowiednikiem const X* x
.
niektórzy wolą styl const
-na-prawo, nazywając go “konsekwentny const
” lub, używając terminu ukutego przez Simona Branda, “Wschód const
.”Rzeczywiście styl” East const
“może być bardziej spójny niż alternatywa: styl” East const
” zawsze obraca const
po prawej stronie tego, co określa, podczas gdy inny styl czasami umieszcza const
po lewej, a czasami po prawej (dla const
deklaracji wskaźnika i const
funkcji Członkowskich).
w stylu “East const
” zmienna lokalna, która jest const
, jest zdefiniowana za pomocąconst
po prawej stronie: int const a = 42;
. Podobnie zmienna static
, która jest const
jest zdefiniowana jako static double const x = 3.14;
.Zasadniczo każdy const
kończy się po prawej stronie rzeczy, którą stanowi, w tym const
, która jest wymagana do uzyskania prawej deklaracji wskaźnika const
i funkcji członka const
.
styl “East const
” jest również mniej mylący, gdy jest używany z aliasami typów: dlaczego foo
i bar
mają tutaj różne typy?
using X_ptr = X*;const X_ptr foo;const X* bar;
użycie stylu “East const
” sprawia, że jest to wyraźniejsze:
using X_ptr = X*;X_ptr const foo;X* const foobar;X const* bar;
wyraźnie widać, że foo
i foobar
to ten sam typ, A bar
to inny typ.
styl “East const
” jest również bardziej spójny z deklaracjami wskaźnika. Kontrast w tradycyjnym stylu:
const X** foo;const X* const* bar;const X* const* const baz;
w przypadku stylu “East const
”
X const** foo;X const* const* bar;X const* const* const baz;
pomimo tych korzyści, styl const
-on-the-right nie jest jeszcze popularny, więc tradycyjny kod ma tendencję do tradycyjnego stylu.
czy “X& const x” ma jakiś sens?
nie, to nonsens.
aby dowiedzieć się, co oznacza powyższa deklaracja, przeczytaj ją od prawej do lewej: “x
jest const
odniesieniem do X
“. Ale to jest zbędne — odniesienia są zawsze const
, w tym sensie, że nigdy nie można ponownie umieścić odniesienia, aby odnieść się do innego obiektu. Nigdy. Z lub bez const
.
innymi słowy, “X& const x
” jest funkcjonalnie odpowiednikiem “X& x
“. Ponieważ nic nie zyskujesz, dodając const
po &
, nie powinieneś go dodawać: to zmyli ludzi — const
sprawi, że niektórzy ludzie pomyślą, że X
to const
, tak jakbyś powiedział “const X& x
“.
co to jest “funkcja const member”?
funkcja składowa, która kontroluje (a nie mutuje) swój obiekt.
a const
funkcja członu jest oznaczana przyrostkiem const
tuż po liście parametrów funkcji członu. Funkcje Członkowskie z przyrostkiem const
nazywane są “const
funkcje Członkowskie” lub “inspektorzy.”Funkcje członkowskie bez przyrostkaconst
nazywane są “funkcjami spozaconst
” lub “mutatorami.”
class Fred {public: void inspect() const; // This member promises NOT to change *this void mutate(); // This member function might change *this};void userCode(Fred& changeable, const Fred& unchangeable){ changeable.inspect(); // Okay: doesn't change a changeable object changeable.mutate(); // Okay: changes a changeable object unchangeable.inspect(); // Okay: doesn't change an unchangeable object unchangeable.mutate(); // ERROR: attempt to change unchangeable object}
próba wywołania unchangeable.mutate()
jest błędem wykrytym podczas kompilacji. Dla const
nie ma miejsca w trybie runtime ani speedpenalty i nie musisz pisać przypadków testowych, aby sprawdzić je w trybie runtime.
końcowa const
na inspect()
funkcja member powinna być użyta, aby metoda nie zmieniła stanu obiektu abstract (Client-visible). To nieco różni się od stwierdzenia, że metoda nie zmieni “surowych bitów” obiektu struct
. Kompilatory C++ nie mogą przyjmować interpretacji “bitowej”, chyba że są w stanie rozwiązać problem, który normalnie nie może zostać rozwiązany (np. może istnieć alias inny niżconst
, który mógłby zmienić stan obiektu). Kolejny (ważny) wgląd w ten problem z aliasingiem: wskazywanie na obiekt wskaźnikiem-do – const
nie gwarantuje, że obiekt się nie zmieni; jedynie obiecuje, że obiekt nie zmieni się za pomocą tego wskaźnika.
jaka jest zależność między funkcją return-by-reference a funkcją const?
jeśli chcesz zwrócić element obiektu this
przez odniesienie z metody Inspektora, powinieneś zwrócić go za pomocą reference-to-const (const X& inspect() const
) lub value (X inspect() const
).
class Person {public: const std::string& name_good() const; // Right: the caller can't change the Person's name std::string& name_evil() const; // Wrong: the caller can change the Person's name int age() const; // Also right: the caller can't change the Person's age // ...};void myCode(const Person& p) // myCode() promises not to change the Person object...{ p.name_evil() = "Igor"; // But myCode() changed it anyway!!}
dobrą wiadomością jest to, że kompilator często cię złapie, jeśli się pomylisz. W szczególności, jeśli przypadkowo zwrócisz członka swojego obiektu this
przez odniesienie inne niżconst
, takie jak w Person::name_evil()
powyżej, kompilator często go wykryje i spowoduje błąd podczas kompilacji wnętrz, w tym przypadku,Person::name_evil()
.
zła wiadomość jest taka, że kompilator nie zawsze cię złapie: są przypadki, w których kompilator po prostu nie zawsze wysyła Ci komunikat o błędzie w czasie kompilacji.
tłumaczenie: trzeba myśleć. Jeśli cię to przeraża, znajdź inną linię pracy; “myśl” nie jest czteroliterowym słowem.
pamiętaj o” const
filozofii ” rozłożonej w tej sekcji: funkcja członka const
nie musi zmieniać (lub zezwalać na zmianę wywołującego) stanu logicznego obiektu this
(AKA stan abstrakcyjny AKA meaningwisestate). Pomyśl o tym, co oznacza obiekt, a nie jak jest on wewnętrznie realizowany. Wiek i imię osoby są logicznie części osoby, ale sąsiad osoby i pracodawca nie są. Metoda Inspektora, która zwraca część stanu logicznego / abstrakcyjnego / meaningwise obiektu this
nie może zwracać wskaźnika (lub referencji) innego niżconst
do tej części,niezależnie od tego, czy ta część jest wewnętrznie zaimplementowana jako bezpośredni element danych fizycznie osadzony w obiekciethis
, czy w inny sposób.
o co chodzi z “const-overloading”?
const
przeciążenie pomaga osiągnąć const
poprawność.
const
przeciążenie jest wtedy, gdy masz metodę inspektora i metodę mutatora o tej samej nazwie i tej samej liczbie i typów parametrów. Dwie różne metody różnią się tylko tym, że theinspector jest const
, a mutator nie jestconst
.
najczęstszym zastosowaniem const
przeciążenia jest operator kodu dolnego. Zazwyczaj powinieneś spróbować użyć jednego ze standardowych szablonów kontenerów, takich jak std::vector
, ale jeśli chcesz stworzyć własną klasę, która ma operatora indeksu, oto zasada: operatory indeksu często występują w parach.
class Fred { /*...*/ };class MyFredList {public: const Fred& operator (unsigned index) const; // Subscript operators often come in pairs Fred& operator (unsigned index); // Subscript operators often come in pairs // ...};
Operator kodu dolnego const
zwraca referencję const
, więc kompilator zapobiegnie przypadkowemu zmienianiu/modyfikowaniu kodu Fred
. Operator indeksu dolnego nieconst
zwraca referencję nieconst
, co jest sposobem, w jaki użytkownicy wywołujący (i kompilator) mogą modyfikować obiekt Fred
.
gdy użytkownik twojej klasy MyFredList
wywoła operatora skryptu dolnego, kompilator wybiera, które przeciążenie ma wywołać na podstawie stałości ich klasy MyFredList
. Jeśli dzwoniący ma MyFredList a
lub MyFredList& a
, to a
wywoła operatora podrzędnego innego niżconst
, a dzwoniący skończy z odniesieniem innego niżconst
do Fred
:
na przykład załóżmy, że class Fred
ma metodę Inspektora inspect() const
i metodę mutatora mutate()
:
void f(MyFredList& a) // The MyFredList is non-const{ // Okay to call methods that inspect (look but not mutate/change) the Fred at a: Fred x = a; // Doesn't change to the Fred at a: merely makes a copy of that Fred a.inspect(); // Doesn't change to the Fred at a: inspect() const is an inspector-method // Okay to call methods that DO change the Fred at a: Fred y; a = y; // Changes the Fred at a a.mutate(); // Changes the Fred at a: mutate() is a mutator-method}
jednak jeśli dzwoniący ma const MyFredList a
lub const MyFredList& a
, to a
zadzwoni do const
subscriptoperator, a dzwoniący skończy z const
odniesieniem do Fred
. Pozwala to na sprawdzenie Fred
w a
, ale zapobiega to przypadkowemu mutowaniu/zmianie Fred
w a
.
void f(const MyFredList& a) // The MyFredList is const{ // Okay to call methods that DON'T change the Fred at a: Fred x = a; a.inspect(); // Compile-time error (fortunately!) if you try to mutate/change the Fred at a: Fred y; a = y; // Fortunately(!) the compiler catches this error at compile-time a.mutate(); // Fortunately(!) the compiler catches this error at compile-time}
przeciążenie Konst dla operatorów indeksów dolnych i funcall jest zilustrowane TUTAJ,TUTAJ, TUTAJ, TUTAJ i tutaj.
możesz oczywiście również użyć const
– overloading dla rzeczy innych niż operator kodu dolnego.
jak może mi pomóc zaprojektować lepsze klasy, jeśli odróżnię stan logiczny od stanu fizycznego?
ponieważ to zachęca do projektowania klas od zewnątrz, a nie od wewnątrz, co z kolei sprawia, że klasy i obiekty są łatwiejsze do zrozumienia i użycia, bardziej intuicyjne, mniej podatne na błędy i szybsze. (OK, to małe uproszczenie. Aby zrozumieć wszystkie “Jeśli” I “ale”, musisz po prostu przeczytać resztę tego tekstu!
zrozummy to od wewnątrz — Na Zewnątrz-będziesz (powinien) projektować swoje klasy od zewnątrz-w, ale jeśli jesteś nowy w tej koncepcji, łatwiej jest to zrozumieć od wewnątrz-Na Zewnątrz.
wewnątrz twoje obiekty mają stan fizyczny (lub konkretny lub bitowy). Jest to stan, który programiści mogą łatwo zobaczyć i zrozumieć; jest to stan, który byłby tam, gdyby klasa była tylko w stylu C struct
.
Na Zewnątrz twoje obiekty mają użytkowników twojej klasy, A ci użytkownicy są ograniczeni do używania tylko public
funkcji Członkowskich i friend
s. Ci zewnętrzni użytkownicy również postrzegają obiekt jako posiadający stan, na przykład, jeśli obiekt jest klasy Rectangle
z metodami width()
, height()
i area()
, Twoi użytkownicy powiedzieliby, że te trzy są częścią logicznego (lub abstrakcyjnego lub znaczącego) stanu obiektu. Dla użytkownika zewnętrznego obiekt Rectangle
ma obszar, nawet jeśli obszar ten jest obliczany w locie (np. jeśli metoda area()
zwróci iloczyn szerokości i wysokości obiektu). W rzeczywistości, i to jest ważny punkt, Twoi użytkownicy nie wiedzą i nie dbają o to, jak wdrożysz którąkolwiek z tych metod; użytkownicy nadal postrzegają, z ich perspektywy, że Twój obiekt ma logiczny stan szerokości, wysokości i powierzchni.
przykład area()
pokazuje przypadek, w którym stan logiczny może zawierać elementy, które nie są bezpośrednio realizowane w stanie fizycznym. Jest również odwrotnie: klasy czasami celowo ukrywają część fizycznego (konkretnego, bitowego) stanu swoich obiektów przed użytkownikami-celowo nie dostarczają żadnych funkcjipublic
lub friend
, które pozwoliłyby użytkownikom czytać lub pisać, a nawet wiedzieć o tym ukrytym stanie. Oznacza to, że w stanie fizycznym obiektu nie ma odpowiednich elementów w stanie logicznym obiektu.
jako przykład tego drugiego przypadku, obiekt kolekcji może buforować swoje ostatnie wyszukiwanie w nadziei na poprawę wydajności następnego wyszukiwania. Ta pamięć podręczna jest z pewnością częścią stanu fizycznego obiektu, ale jest to detal wewnętrznej implementacji, który prawdopodobnie nie będzie wystawiony na działanie użytkowników-prawdopodobnie nie będzie częścią stanu logicznego obiektu. Mówienie, co jest łatwe, jeśli myślisz z zewnątrz-w: jeśli użytkownicy collection-object mają teraz możliwość sprawdzenia stanu samego bufora, to bufor jest przezroczysty i nie jest częścią stanu logicznego obiektu.
czy stałość moich funkcji Public member powinna być oparta na tym,co metoda robi ze stanem logicznym obiektu, czy fizycznym?
logiczne.
nie ma sposobu, aby ta następna część była łatwa. To będzie bolało. Najlepiej usiąść. I proszę, dla Twojego bezpieczeństwa, upewnij się, że w pobliżu nie ma ostrych narzędzi.
wróćmy do przykładu collection-object. Pamiętaj: istnieje metoda wyszukiwania, która pobiera ostatnie wyszukiwanie w nadziei na przyspieszenie przyszłych wyszukiwań.
powiedzmy to, co jest prawdopodobnie oczywiste: Załóżmy, że metoda lookup nie zmienia żadnego stanu logicznego obiektu Collection-object.
więc … nadszedł czas, aby cię zranić. Gotowy?
oto nadchodzi: jeśli metoda lookup nie wprowadza żadnych zmian w stanie logicznym obiektu collection-object, ale zmienia stan fizyczny obiektu collection-object (powoduje bardzo realną zmianę w bardzo realnym buforze), czy metoda lookup powinna być const
?
odpowiedź brzmi zdecydowanie tak. (Istnieją wyjątki od każdej reguły, więc “tak” powinno mieć gwiazdkę obok,ale w zdecydowanej większości przypadków odpowiedź brzmi “tak”.)
tu chodzi o “logiczne const
” nad “fizyczne const
.”Oznacza to, że decyzja o tym, czy udekorować amethod const
powinna zależeć przede wszystkim od tego, czy ta metoda pozostawi stan logiczny niezmieniony, niezależnie od tego(czy siedzisz?) (możesz chcieć usiąść) niezależnie od tego, czy metoda robi bardzo realchanges do bardzo rzeczywistego stanu fizycznego obiektu.
na wypadek, gdyby ci się nie udało, albo gdybyś jeszcze nie cierpiał, Podzielmy to na dwa przypadki:
- jeśli metoda zmienia jakąkolwiek część stanu logicznego obiektu, logicznie jest mutatorem; nie powinna być
const
evenif (jak to się faktycznie dzieje!) metoda nie zmienia żadnych fizycznych bitów konkretnego stanu obiektu. - odwrotnie, metoda jest logicznie inspektorem i powinna być
const
, jeśli nigdy nie zmienia żadnej części stanu logicznego obiektu, nawet jeśli (tak się dzieje!) metoda zmienia fizyczne bity konkretnego stanu obiektu.
jeśli jesteś zdezorientowany, przeczytaj to jeszcze raz.
jeśli nie jesteś zdezorientowany, ale jesteś zły, dobrze: możesz tego jeszcze nie lubić, ale przynajmniej to rozumiesz. Weź głęboki oddech i powtórz za mną: “const
ness metody powinien mieć sens z zewnątrz obiektu.”
jeśli nadal jesteś zły, powtórz to trzy razy: “stałość metody musi mieć sens dla użytkowników obiektu, a ci użytkownicy mogą zobaczyć tylko stan logiczny obiektu.”
jeśli nadal jesteś zły, sorry, jest jak jest. Weź się w garść i żyj z tym. Tak, będą wyjątki; każda zasada jest ich. Ale z reguły, w głównej mierze, to logiczne const
pojęcie jest dobre dla Ciebie i dobre dla Twojego oprogramowania.
jeszcze jedno. To będzie nieinane, ale bądźmy dokładni, czy metoda zmienia stan logiczny obiektu. Jeśli jesteś poza klasą-jesteś normalnym użytkownikiem, każdy eksperyment, który możesz wykonać (każda metoda lub Sekwencja metod, które wywołujesz) będzie miał takie same wyniki (te same wartości zwracane, te same wyjątki lub brak WYJĄTKÓW)niezależnie od tego, czy najpierw wywołałeś tę metodę wyszukiwania. Jeżeli funkcja lookup zmieniła jakiekolwiek przyszłe zachowanie każdej przyszłej metody (nie tylko przyspieszając jej działanie, ale zmieniając wynik, zmieniając wartość zwracaną, zmieniając exception), to metoda lookup zmieniła stan logiczny obiektu — jest to mutuator. Ale jeśli metoda lookupzmieniła nic innego, jak być może przyspieszyć niektóre rzeczy, to jest inspektorem.
co zrobić, jeśli chcę, aby funkcja członka const dokonała “niewidocznej” zmiany na członie danych?
użyj mutable
(lub, w ostateczności, użyj const_cast
).
niewielki odsetek inspektorów musi wprowadzić zmiany w stanie fizycznym obiektu, których nie mogą zaobserwować zewnętrzni użytkownicy — zmiany w stanie fizycznym, ale nie logicznym.
na przykład omawiany wcześniej obiekt collection-object buforował swoje ostatnie wyszukiwanie w nadziei poprawienia wydajności następnego wyszukiwania. Ponieważ cache, w tym przykładzie, nie może być bezpośrednio obserwowany przez żaden publiczny interfejs kolekcji-obiektu (inny niż czas), jego istnienie i stan nie są częścią stanu logicznego obiektu, więc zmiany w nim są niewidoczne dla zewnętrznych użytkowników. Metoda lookup jest inspektorem, ponieważ nigdy nie zmienia stanu logicznego obiektu, niezależnie od tego, że, przynajmniej dla obecnej implementacji, zmienia stan fizyczny obiektu.
gdy metody zmieniają stan fizyczny, ale nie logiczny, metoda powinna być ogólnie oznaczona jako const
, ponieważ jest to metoda-Inspektor. Tworzy to problem: gdy kompilator zobaczy Twoją metodę const
zmieniającą stan fizyczny obiektu this
, będzie narzekał — wyświetli twojemu kodowi komunikat o błędzie.
język kompilatora C++ używa słowa kluczowego mutable
, aby pomóc ci objąć to logiczne pojęcie const
. W takim przypadku należy oznaczyć bufor słowem kluczowym mutable
, dzięki czemu kompilator wie, że można go zmienić wewnątrz metodyconst
lub za pomocą dowolnego innego wskaźnika lub referencji const
. W naszym żargonie słowo kluczowe mutable
oznacza te porcje stanu fizycznego obiektu, które nie są częścią stanu logicznego.
słowo kluczowe mutable
znajduje się tuż przed deklaracją członka danych, czyli w tym samym miejscu, w którym można umieścićconst
. Innym podejściem, nie preferowanym, jest odrzucenie const
‘ness wskaźnika this
, prawdopodobnie za pomocą słowa kluczowegoconst_cast
:
Set* self = const_cast<Set*>(this); // See the NOTE below before doing this!
po tej linii, self
będzie miał te same bity co this
, to znaczy self == this
, ale self
jest Set*
zamiastconst Set*
(technicznie this
jest const Set* const
, ale prawo-większość const
nie ma znaczenia dla tej dyskusji).Oznacza to, że możesz użyć self
, aby zmodyfikować obiekt wskazywany przez this
.
Uwaga: Istnieje bardzo mało prawdopodobny błąd, który może wystąpić w przypadku const_cast
. Dzieje się tak tylko wtedy, gdy trzy bardzo rzadkie przypadki są połączone w tym samym czasie: element danych, który powinien być mutable
(taki jak omówiono powyżej), kompilator, który nie obsługuje słowa kluczowego mutable
i/lub programista, który go nie używa, oraz obiekt, który został pierwotnie zdefiniowany jako const
(w przeciwieństwie do normalnego, Nie-const
obiektu, na który wskazuje wskaźnik-do-const
).Chociaż ta kombinacja jest tak rzadka, że może nigdy ci się nie przydarzyć, jeśli kiedykolwiek się zdarzy, kod może nie działać (standard mówi, że zachowanie jest niezdefiniowane).
jeśli kiedykolwiek chcesz użyć const_cast
, użyj mutable
zamiast tego. Innymi słowy, jeśli kiedykolwiek trzeba zmienić element anobject, a Obiekt ten jest wskazywany przez wskaźnik-do – const
, najbezpieczniejszą i najprostszą rzeczą do zrobienia jest dodanie mutable
do deklaracji członka. Możesz użyć const_cast
, jeśli jesteś pewien, że obiekt nie jest const
(np. jeśli jesteś pewien, że obiekt jest zadeklarowany tak: Set
s;
), ale jeśli sam obiekt może być const
(np. ifit może być zadeklarowany tak: const Set s;
), użyj mutable
zamiast const_cast
.
proszę nie pisać, że wersja X kompilatora Y na maszynie z Pozwala na zmianę elementu nie-mutable
obiektuconst
. Nie obchodzi mnie to-jest to nielegalne w zależności od języka i twój kod prawdopodobnie zawiedzie na innym kompilatorze lub nawet innej wersji (upgrade) tego samego kompilatora. Po prostu powiedz nie. Zamiast tego użyj mutable
. Napisz kod, który gwarantuje działanie, a nie kod, który nie wydaje się łamać.
czy const_cast oznacza utracone możliwości optymalizacji?
w teorii tak, w praktyce nie.
nawet jeśli język zakazał const_cast
, jedynym sposobem uniknięcia przepłukiwania pamięci podręcznej rejestru w wywołaniu memberfunction const
byłoby rozwiązanie problemu aliasingu (tj., aby udowodnić, że nie ma wskaźników innych niżconst
, które wskazywałyby na obiekt). Może się to zdarzyć tylko w rzadkich przypadkach (gdy obiekt jest skonstruowany w zakresie wywołania funkcji const
i gdy wszystkie wywołania funkcji nieconst
między konstrukcją obiektu a wywołaniem funkcjiconst
są statycznie związane, i gdy każde z tych wywołań jest również inline
d, a sam konstruktor jest inline
d, A gdy dowolne funkcje członowe wywołane przez konstruktora są inline
).
dlaczego kompilator pozwala mi zmienić int po wskazaniu na niego const int*?
bo “const int* p
“oznacza” p
obiecuje nie zmieniać *p
, “nie” *p
obiecuje nie zmieniać.”
powoduje, że const int*
wskazuje na int
nie const
-jeśli int
. int
nie można zmienić za pomocąconst int*
, ale jeśli ktoś inny ma int*
(uwaga: nie const
), który wskazuje na (“aliasy”) to samo int
, toint*
może być użyty do zmiany int
. Na przykład:
void f(const int* p1, int* p2){ int i = *p1; // Get the (original) value of *p1 *p2 = 7; // If p1 == p2, this will also change *p1 int j = *p1; // Get the (possibly new) value of *p1 if (i != j) { std::cout << "*p1 changed, but it didn't change via pointer p1!\n"; assert(p1 == p2); // This is the only way *p1 could be different }}int main(){ int x = 5; f(&x, &x); // This is perfectly legal (and even moral!) // ...}
zauważ, że main()
i f(const int*,int*)
mogą być w różnych jednostkach kompilacji, które są kompilowane w różnych dniach tygodnia. W takim przypadku kompilator nie jest w stanie wykryć aliasingu podczas kompilacji. Dlatego nie ma sposobu, abyśmy mogli stworzyć regułę językową, która zabraniałaby tego rodzaju rzeczy. W rzeczywistości nie chcielibyśmy nawet tworzyć takiego arule, ponieważ ogólnie jest to funkcja, która może mieć wiele wskaźników wskazujących na to samo. Fakt, że jeden z tych wskaźników obiecuje, że nie zmieni podstawowej “rzeczy”, jest tylko obietnicą złożoną przez wskaźnik; to nie jest obietnica złożona przez “rzecz”.
czy “const * P” oznacza, że * p nie może się zmienić?
Nie! (Jest to związane z FAQ dotyczącym aliasowania wskaźników int
.)
“const Fred* p
” oznacza, że Fred
nie można zmienić za pomocą wskaźnika p
, ale mogą istnieć inne sposoby dotarcia do obiektu bez przechodzenia przez const
(takie jak aliasowany wskaźnik inny niżconst
, taki jak Fred*
). Na przykład, jeśli masz dwa wskaźniki “const Fred* p
” i “Fred* q
“, które wskazują na ten sam obiekt Fred
(aliasing), wskaźnik q
może być użyty do zmiany obiektu Fred
, ale wskaźnik p
nie może.
class Fred {public: void inspect() const; // A const member function void mutate(); // A non-const member function};int main(){ Fred f; const Fred* p = &f; Fred* q = &f; p->inspect(); // Okay: No change to *p p->mutate(); // Error: Can't change *p via p q->inspect(); // Okay: q is allowed to inspect the object q->mutate(); // Okay: q is allowed to mutate the object f.inspect(); // Okay: f is allowed to inspect the object f.mutate(); // Okay: f is allowed to mutate the object // ...}
dlaczego otrzymuję błąd konwersji Foo** → const Foo**?
ponieważ konwersja Foo**
→ const Foo**
byłaby Nieprawidłowa i niebezpieczna.
C++ pozwala na (bezpieczną) konwersję Foo*
→ Foo const*
, ale daje błąd, jeśli próbujesz pośrednio przekonwertować Foo**
→const Foo**
.
uzasadnienie tego błędu jest podane poniżej. Ale najpierw jest najczęstsze rozwiązanie: simplychange const Foo**
do const Foo* const*
:
class Foo { /* ... */ };void f(const Foo** p);void g(const Foo* const* p);int main(){ Foo** p = /*...*/; // ... f(p); // ERROR: it's illegal and immoral to convert Foo** to const Foo** g(p); // Okay: it's legal and moral to convert Foo** to const Foo* const* // ...}
powodem, dla którego konwersja z Foo**
→ const Foo**
jest niebezpieczna, jest to, że pozwoli Ci cicho i przypadkowo zmodyfikować const Foo
obiekt bez obsady:
class Foo {public: void modify(); // make some modification to the this object};int main(){ const Foo x; Foo* p; const Foo** q = &p; // q now points to p; this is (fortunately!) an error *q = &x; // p now points to x p->modify(); // Ouch: modifies a const Foo!! // ...}
jeśli linia q = &p
byłaby legalna, q
wskazywałaby na p
. Następna linia, *q = &x
, zmienia samą p
(ponieważ *q
to p
) na x
. To byłoby złe, ponieważ przegraliśmy kwalifikator const
: p
to Foo*
, alex
to const Foo
. Linia p->modify()
wykorzystuje zdolność p
do modyfikowania swojego referenta, co jest prawdziwym problemem,ponieważ skończyło się na modyfikacji const Foo
.
w drodze analogii, jeśli ukryjesz przestępcę pod legalnym przebraniem, może on wykorzystać zaufanie, jakim obdarzył to przebranie.To źle.
na szczęście C++ uniemożliwia ci to: linia q = &p
jest oznaczana przez kompilator C++ jako compile-timeerror. Przypomnienie: proszę nie wskazywać-cast swój sposób wokół tego komunikatu o błędzie kompilacji. Po Prostu Powiedz Nie!
(Uwaga: Istnieje koncepcyjne podobieństwo między tym a zakazem konwersji Derived**
naBase**
.)