niedziela, 29 sierpnia 2010

Wszystko co musisz wiedzieć o const w C++

const w C++ to jeden z najczęstszych modyfikatorów. Jest z nim związane wiele "ciekawostek" i niuansów, więc dzisiaj ten temat w pełni przemielimy ;)

const jak sama nazwa wskazuje służy w oryginalnym zamyśle do definiowania stałych. Maniacy C pewnie nadal używają #define, ale normalni ludzie napiszą w C++:

const int INFINITY = 2000000000;

nad #define INFINITY 2000000000; konstrukcja ta ma jedną, znaczącą przewagę - typ. Dzięki temu, że kompilator wie, że to int, może nas w porę ostrzec o jakiejś głupocie. Ale ludzie od C++ nie byliby sobą, gdyby const miało tylko takie jedno, proste znaczenie.

1. "Wskaźniki na stałe" i "stałe wskaźniki"

Taki kawałek kodu nam nie zadziała:

const int INFINITY = 2000000000;
int* p = &INFINITY;


Dlaczego? Ponieważ p jest wskaźnikiem na liczbę, która jest stała. Nie możemy jej zmienić, a typ wskaźnika p nie zawiera takiej informacji. Dopiero jedna modyfikacja:

const int INFINITY = 2000000000;
const int* p = &INFINITY;


lub jako kto woli:

const int INFINITY = 2000000000;
int const* p = &INFINITY;


definiuje nam p jako wskaźnik na stałą. Typ const T* oznacza, że to, na co wskazuje ten wskaźnik trzeba traktować jak stałą - czyli nie można modyfikować. Nie możemy więc napisać

const int* p = &INFINITY;
*p = 7;


Oczywiście elementem wskazywanym nie musi koniecznie być stała, np.:

int x = 42;
const int* p = &x;


jest jak najbardziej poprawne i logiczne. Możemy też zmieniać elementy na które wskazuje p, ważne jest tylko niemodyfikowanie wskazywanych elementów:

const int* p = &x;
p = &INFINITY;


W powyższych przykładach widzimy jeszcze jedną przewagę const nad #define - z #define INFINITY 2000000000; powyższe przykłady nie są możliwe do skompilowania ;)

"Wskaźnik na stałą" to nie jest jedyna możliwość. Zmodyfikujmy powyższy fragmencik zmieniając położenie modyfikatora const:

int x = 42;
int* const p = &x;


Kiedy const pojawia się po gwiazdce, to mamy stały wskaźnik. Czyli możemy modyfikować element, na który wskazuje p, ale nie możemy zmienić samego p - ten wskaźnik będzie już zawsze wskazywał na ten sam adres w pamięci. Czyli teraz:

p = new int(8);

nie jest poprawne, ponieważ p nie można zmieniać. Natomiast bez problemów możemy zmienić to na co wskazuje p:

*p = 8;

Można spotkać się z połączeniem obu tych koncepcji - "stałym wskaźnikiem do stałej":

const int* const p = &INFINITY;

Łączy on w sobie obostrzenia obu wersji - czyli nie możemy zmieniać zarówno tego na co wskazuje p, jak i samego wskaźnika.


2. Referencje do stałych

Koncepcja jest taka sama jak w przypadku wskaźników na stałe, tylko miejsce gwiazdek zajmują ampersandy ;)

Kiedy więc napiszemy

const int& r = INFINITY;

lub

int const& r = INFINITY;

(const po lewej stronie ampersandu), to już nie możemy napisać:

r = 7;

ponieważ r jest referencją do stałej. Oczywiście podobnie jak przy wskaźnikach to, do czego się odnosi r nie musi być stałą, ale tak będzie traktowane.

OK, skoro jest pełna analogia, to pewnie mamy też "stałe referencje".

int x = 7;
int& const p = x;


To co napisałem powyżej jest niepoprawne - skompiluje się z ostrzeżeniem o anachronizmie, więc dobrze to to nie jest. Dlaczego? Ponieważ referencje z definicji są stałe - dodatkowy modyfikator const jest zbędny.


3. const w deklaracjach metod

Dwa poprzednie punkty mają największe znaczenie dla typów argumentów w deklaracjach metod. I tak, w ramach krótkiego przypomnienia:

void f1(int* const a) - stały wskaźnik pozwoli nam zrobić *a = 8;, ale już nie można a = new int(5);.


void f2(const int* a) - wskaźnik na stałą zabroni nam zrobić *a = 8;, ale można a = new int(5);.

void f3(const int* const a) - stały wskaźnik na stałą zabroni nam zrobić *a = 8;, a także nielegalne będzie a = new int(5);.

void f4(const int& a) - stała referencja uniemożliwi wykonanie a = 8;.

Na const przy referencjach warto uważać - bez tego modyfikatora w funkcji f4 nie można by wykonać następującego kodu:

f4(42);

Warto więc na const uważać ;) I standardowe przypomnienie - const int& a to najszybszy sposób przekazywania argumentów do funkcji :)

Dodatkowa uwaga przy typach zwracanych metod - jeżeli napiszemy funkcję:

char* hello() { return "Hello world!"; }

to ktoś może użyć jej tak:

char* s = hello();
s[1] = 'a';


I bum, bo s tak naprawdę wskazuje na stałą "Hello world!". Rozwiązaniem jest poprawa typu zwracanego funkcji hello():

const char* hello() { return "Hello world!"; }

Warto o tym pamiętać ;)


4. Metody const

Odwołam się tutaj do przykładu z mojego posta sprzed jakiegoś czasu nt. rzadko spotykanych słów kluczowych w C++ (http://wojtek-m.blogspot.com/2010/07/rzadko-spotykane-sowa-kluczowe-c.html). Załóżmy że piszemy klasę do obsługi macierzy - skupmy się na funkcji wyliczającej wyznacznik macierzy:

// Matrix.h
class Matrix {
  public:
    double getDeterminant();
    //...
};

// Matrix.cpp
double Matrix::getDeterminant() {
  //...
  return 12.0;
}


Teraz mamy funkcję void f(const Matrix& mtrx), która wyświetli na ekranie wartość wyznacznika. Jest pewien problem - macierz mtrx została przekazana jako stała, nie można jej więc modyfikować. A metoda double getDeterminant() teraz może dokonać takiej modyfikacji... Co poradzić?

Otóż wystarczy oznaczyć tą funkcję jako niemodyfikującą obiektu danej klasy - czyli działającą również dla stałych. Służy do tego (jak się słusznie domyśliliście) modyfikator const w odpowiednim miejscu:

// Matrix.h
class Matrix {
  public:
    double getDeterminant() const;
    //...
};

// Matrix.cpp
double Matrix::getDeterminant() const {
  //...
  return 12.0;
}


Voila! Technicznie rzecz biorąc w metodach bez modyfikatora const wskaźnik this jest typu Matrix* const (czyli stały wskaźnik). Po dodaniu modyfikatora const do metody wskaźnik this w niej jest typu const Matrix* const, czyli jest to stały wskaźnik do stałej.

Można sobie wyobrazić następującą metodę klasy:

const char* f(const int* const a) const;

To na pewno powinien być "stały element" klasy ;)


5. Atrybuty const

O skoro już o stałych elementach klasy mowa, to nie można zapomnieć o stałych atrybutach w klasie. Mają one sens podobny do atrybutów final w Javie - ustawiamy je raz, na początku, a potem nie można ich modyfikować:

class A
{
  public:
    const int x;
    //...
};


Pomysł zacny, ale jak go zrealizować? Pierwsze podejście:

A(int _x)
{
  x = _x;
  //...
}


skończy się błędem kompilacji. Dlaczego? Jak (jeśli w ogóle) pamiętacie z ostatniego odcinka serii o wydajności w C++, wszystkie pola klasy są inicjowane wartościami domyślnymi przed wejściem do konstruktora. Czyli atrybut x dostał już wartość 0. Wartościami domyślnymi lub wartościami podanymi na liście inicjalizacyjnej. Rozwiązaniem jest więc...

A(int _x) : x(_x)
{
  //...
}


Świat ponownie został uratowany ;)


6. Obejścia - mutable i const_cast

Jeżeli przeczytałeś już wszystko, to może nasunęły Ci się dwa pytania-problemy:

1. Co zrobić, kiedy metoda jest i powinna być const, a my chcemy coś jednak zmodyfikować. Dobrym przykładem będzie już wspomniana klasa macierzy - kiedy raz wyliczymy wyznacznik możemy go zapamiętać, a potem zwracać tylko zapamiętaną wartość:

// Matrix.h
class Matrix {
  public:
    double getDeterminant() const;
    //...
  private:
    double calculateDet() const;
    double det;
    bool isDetValid;
    //...
};

// Matrix.cpp
double Matrix::getDeterminant() const {
  if (!isDetValid) {
    det = calculateDet();
    isDetValid = true;
  }
  return det;
}

double Matrix::calculateDet() const {
  //...
  return 12.0;
}


I mamy problem kompilacji. getDeterminant() jest metodą const, czyli nie zmieniającą stanu obiektu. A modyfikuje przecież dwie zmienne.

Rozwiązaniem jest słowo kluczowe mutable - jest to modyfikator, który może być stosowany do składowych klasy, które są niestatycznymi i niestałymi atrybutami. Mówi on: "OK, ten atrybut będzie można modyfikować w metodach const, bo on tak naprawdę nie ustala stanu obiektu".

Kod z tym słowem kluczowym będzie kompilował się już bez problemów i działał tak jak chcemy:

// Matrix.h
class Matrix {
  public:
    double getDeterminant() const;
    //...
  private:
    double calculateDet() const;
    mutable double det;
    mutable bool isDetValid;
    //...
};

// Matrix.cpp
double Matrix::getDeterminant() const {
  if (!isDetValid) {
    det = calculateDet();
    isDetValid = true;
  }
  return det;
}

double Matrix::calculateDet() const {
  //...
  return 12.0;
}



A teraz zagadka: Wiadomo, że modyfikator mutable nie może być stosowany do stałych atrybutów klasy. To dlaczego poniższy kod kompiluje się?

mutable const Matrix* ptr;

Zwycięzca dostanie ode mnie piwo :) Zagadka od półtora miesiąca leży nierozwiązana (była już w poście http://wojtek-m.blogspot.com/2010/07/rzadko-spotykane-sowa-kluczowe-c.html), więc oprócz piwa nagrodą będzie splendor ;)


I drugie pytanie-problem:

2. Co zrobić, kiedy korzystam z biblioteki, której autor nie słyszał o wskaźnikach na stałe? Zwłaszcza boli to w przypadku argumentów metod... Wiem, że dana metoda nie modyfikuje wskazywanego elementu, ale nadal nie mogę jej użyć ze swoim kodem...

Czyli mamy podobną funkcję biblioteczną:

void f(int* a)
{
  cout << *a << endl;
}


I chcielibyśmy użyć jej tak:

f(&INFINITY);

ale nie mamy takiej możliwość, bo INFINITY jak wiadomo jest const.

Rozwiązaniem jest operator const_cast. Służy on do specyficznego rzutowania - rzutowania, które usuwa lub dodaje modyfikator const lub volatile. Dzięki temu operatorowi możemy dowolnie dodawać lub usuwać te modyfikatory - i to bezkarnie, w czasie kompilacji. Napiszmy więc:

f(const_cast<int*>(&INFINITY));

i gotowe - nasz const-świadomy kod działa z jakimiś antykami :)

Jedyne na co trzeba uważać, to funkcja f2:

void f2(int* a)
{
  *a = 42;
  cout << *a << endl;
}


Wykonanie kodu:

f2(const_cast<int*>(&INFINITY));

zakończy się w sposób niekontrolowany - najpewniej wyjątkiem "Access Violation". Dlatego stosujmy operator const_cast tylko kiedy jesteśmy pewni, że dana funkcja nie wykonuje żadnych modyfikacji.


Więcej:
http://duramecho.com/ComputerInformation/WhyHowCppConst.html
http://www.serc.iisc.ernet.in/ComputingFacilities/systems/cluster/vac-7.0/html/language/ref/clrc05keyword_const_cast.htm
http://msdn.microsoft.com/en-us/library/4h2h0ktk%28VS.90%29.aspx
http://en.wikipedia.org/wiki/Const-correctness - ten artykuł warto przeczytać w całości.

7 komentarzy:

  1. bedzie static w nastepnym odcinku? :D

    OdpowiedzUsuń
  2. co do wstepu - const ma jeszcze jedna przewage nad #define - mozna podejrzec wartosc w trakcie debugowania ;)

    OdpowiedzUsuń
  3. :) Zobaczymy co będzie w kolejnym odcinku :)

    OdpowiedzUsuń
  4. void fun(char a[], char b[], char c[]){
    int i = 0; bool p = false; int pC = 0; int nC = 0;
    for(; ;i++){
    if(!(a[i])) { c[nC] = b[pC] = a[i]; break; }
    (a[i]==' ')?(p=!p,c[nC]=a[i],nC+=1,(!pC)?(pC=pC):(b[pC]=a[i],pC+=1)):(p?(pC+=1,b[pC-1]=a[i]):(nC+=1,c[nC-1]=a[i]));
    }
    }

    jebie mi :D

    OdpowiedzUsuń
  5. Boguś lepiej np. pójdź do kina :P

    OdpowiedzUsuń
  6. Odpowiedź do zagadki:

    mutable const Matrix* ptr;

    namespace matrix {

    class Matrix;

    };

    class matrix::Matrix {

    };

    class DeclMatrix : public matrix::Matrix {


    mutable const matrix::Matrix* ptr_; //1

    mutable matrix::Matrix const* ptr; //2

    // powyższe składowe (ptr i ptr_) - są tego samego typu
    // (kolejność modyfikatora const jest tutaj
    // roźna ale nie wnosi zmian formalnych)

    // W odpowiedzi do zagadki przy [*2*] - widać że rozwiązanie nasuwa się samo
    // modyfikator mutable odnosi się do wartości pod wskaźnikiem - a nie do wartości
    // adresu wskaźnika

    mutable const matrix::Matrix*const const_ptr; // nieprawidłowo

    // Kod powyżej jest nieprawidłowy - zlepek mutable*const jest niedozwolony
    // ale zlepek mutable const const* już jest dozwolony bo znaczeniowo jest róźny


    // Wynika to z kolejności rozwinięcia kolejności modyfikataorów względem implementacji
    // typów wskaźnikowych


    };

    OdpowiedzUsuń