sobota, 10 lipca 2010

Rzadko spotykane słowa kluczowe C++

W C++ można znaleźć mnóstwo słów kluczowych. Jednak niektóre są częściej używane, a niektóre zupełnie rzadko. Czasami warto się zainteresować takimi "białymi krukami" wśród słów kluczowych, aby potem nie być zaskoczonym ;)

Pokrótce opiszę 3 słowa kluczowe, które są najrzadziej spotykane lub ich prawdziwe przeznaczenie nie jest do końca znane ;) Te słowa to typename, explicit i mutable.

1. typename

To słowo kluczowe akurat nie jest tak rzadko spotykane, ale zazwyczaj stosowane jest w innym miejscu, niż miało być ;) Zazwyczaj widzimy je w takim kontekście:

template<typename T>
void f() {
  //...
}


W miejscu typename można zamiennie stosować słowo kluczowe class - nie ma między nimi żadnej różnicy (chyba że ktoś wykorzystuje kompilator sprzed 20 lat). Dlaczego dwie możliwości? Podczas projektowania szablonów dla języka C++ pojawił się problem jakiego słowa kluczowego użyć przy deklaracji szablonu. Aby nie tworzyć nowego wybór padł na class, czyli mieliśmy:

template<classT>
void f() {
  //...
}


Jednak spróbujmy teraz wykorzystać typ zdefiniowany w klasie, która będzie parametrem szablonu:

#include <cstdio>

using namespace std;

class A {
  public:
    typedef int NUMBER;
};

class B {
  public:
    typedef char NUMBER;
};

template<class T>
void f() {
  printf("%d\n", sizeof(T::NUMBER));
}

int main() {
  f<A>();
  f<B>();
  return 0;
}


I mamy problem - kompilator nie może jednoznacznie stwierdzić, czy T::NUMBER dotyczy składowej statycznej czy definicji typu. Dlatego przy wykorzystywaniu typów zdefiniowanych w klasach będących parametrami szablonów trzeba poprzedzić je słowem kluczowym typename, aby kompilator wiedział o co chodzi :) :


template<class T>
void f() {
  printf("%d\n", sizeof(typename T::NUMBER));
}


Tak też przy okazji pojawiło się słowo kluczowe, które lepiej pasuje do definicji szablonu niż class. Niektórzy do dziś stosują rozróżnienie, pisząc class tam gdzie parametrem może być tylko klasa, a typename tam gdzie może być też to jakiś typ podstawowy. Ale jest to tylko konwencja, z punktu widzenia kompilatora nie ma żadnej różnicy.

Więcej: http://msdn.microsoft.com/en-us/library/8y88s595%28VS.90%29.aspx


2. explicit

Załóżmy, że piszemy klasę, która ma reprezentować zbiór o określonej startowej pojemności. Napiszemy pewnie tak:

// Set.h
class Set {
  public:
    Set(int size);
  //...
};

// Set.cpp
Set::Set(int size) {
  //...
}


OK, ale teraz ktoś chce użyć naszej klasy, pisze więc...

int main() {
  Set s = 9;
  //...
}


Ups! Kompilacja się udała, zbiór ma rozmiar początkowy 9, ale to raczej nie o to chodziło piszącemu (pewnie pomyliło mu się z intem). Ale dlaczego to działa?

Ponieważ mamy konstruktor publiczny, który przyjmuje 1 parametr typu int. Wiedząc to kompilator dokonuje niejawnej konwersji przy inicjowaniu nowego obiektu typu Set. Jak się przed tym zabezpieczyć? Służy do tego (zaskoczenie!) słowo kluczowe explicit. Jeżeli poprzedzi ono deklarację konstruktora, to może on zostać wywołany tylko jawnie, nie może być on wykorzystany przy żadnej niejawnej (implicit) konwersji. Dodając to słowo kluczowe otrzymujemy:

// Set.h
class Set {
  public:
    explicit Set(int size);
  //...
};

// Set.cpp
Set::Set(int size) {
  //...
}


I teraz użytkownik będzie dostawał po łapach:

int main() {
  // Set s = 9; // błąd kompilacji - niedopuszczalna konwersja
  Set s(9); // OK!
  //...
}


Więcej: http://msdn.microsoft.com/en-us/library/h1y7x448%28VS.90%29.aspx


3. mutable

Powiedzmy że piszemy klasę do obsługi macierzy. Zaczniemy pewnie tak:

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

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


OK, ale jeżeli często potrzebujemy wyznacznika, a macierz jest praktycznie niezmienna, to aby oszczędzić pracy procesorowi możemy po pierwszym wyliczeniu zapamiętać wynik tak długo, jak długo macierz się nie zmienia. Piszemy więc:

// 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ązania tego problemu są dwa:

1. Usunięcie modyfikatora const - właściwie jest to półśrodek, który często nie jest możliwy do zastosowania. Po usunięciu tego modyfikatora obiekt naszej klasy nie będzie mógł być przekazywany przez stałą referencję, np. do funkcjiktóra wykorzystuje metodę getDeterminant(). Często przekazywania parametru przez stałą referencję wymagają biblioteki, np. kontenery w STL.

2. Słowo kluczowe mutable (tada!) - jest modyfikator, który może być stosowany do składowych klasy, które są niestatycznymi, 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;
}


Więcej: http://msdn.microsoft.com/en-us/library/4h2h0ktk%28VS.90%29.aspx


I na koniec zagadka: wiadomo, że modyfikatory const i mutable nie mogą być stosowane jednocześnie. To dlaczego poniższe konstrukcje kompilują się?

mutable const Matrix* ptr;
const mutable Matrix* ptr;

2 komentarze:

  1. 3 lata i nikt nie rozwiązał tak prostej zagadki? W tych konstrukcjach const dotyczy elementu docelowego, a mutable samego wskaźnika. Z tego powodu te konstrukcje są prawidłowe, ale taka:
    mutable Matrix* const ptr;
    ..już nie, bo przecież "modyfikatory const i mutable nie mogą być stosowane jednocześnie" ;)

    OdpowiedzUsuń