C++11: Funkcje usunięte (= delete)

Programowanie

Dzisiaj postanowiłem napisać krótko o usuwaniu z klas funkcji, które są domyślnie definiowane (i deklarowane) przez kompilator. Powodem tego jest fakt iż w czasie prac nad biblioteką Tamandua, brak usunięcia domyślnie definiowanego konstruktora kopiującego spowodował bardzo trudny do znalezienia błąd. Ponieważ jego zlokalizowanie kosztowało mnie około godziny intensywnej pracy z GNU debuggerem, postanowiłem, że od dzisiaj będę o takie niuanse dbał zaraz po stworzeniu nowej klasy.

Na początek stwórzymy przykładową klasę, która nie będzie miała jawnie zdefiniowanych konstruktorów.

1 class A {
2      private:
3           int n;
4      public:
5           int f();
6 };

Kompilator w przypadku nienapotkania następujących funkcji w deklaracji klasy, zadeklaruje i zdefiniuje je sam:

  1. domyślny konstruktor: A::A();
  2. konstruktor kopiujący: A::A(const A&);
  3. konstruktor przenoszący: A::A(A&&);
  4. operator przypisania kopiujący: A::operator=(const A&);
  5. operator przypisania przenoszący: A::operator=(A&&);

Kilka słów o konstruktorach

Dla osób, które stawiają swoje pierwsze kroki w C++ niektóre z podanych wyżej funkcji mogą wyglądać tajemniczo. W tej części artykułu opiszę krótko każdy z nich.

  1. Domyślny konstruktor jest wywoływany przy tworzeniu nowego obiektu danej klasy. Jak wiadomo, możemy zdefiniować swoje konstruktory przyjmujące różne argumenty, ale ten domyślny jest definiowany automatycznie w przypadku gdy sami nie zdefiniujemy żadnego konstruktora.
  2. Konstruktor kopiujący służy do skopiowania składowych z innego obiektu tego samego typu. Przykładem wywołania: A e1(); A e2(e1); // wywołanie konstruktora kopiującego
  3. Konstruktor przenoszący wykorzystuje tak zwane "rvalue references" (_referencje do r-wartości zostały opisane tutaj). Jest to nowość wprowadzona w C++11 razem z konstruktorami przenoszącymi. Temat zasługuje na osobny artykuł, ale na krótkim przykładzie postaram się objaśnić jak działa przenoszenie. Wyobraźmy sobie, że tworzymy zmienną int i przypisujemy do niej jakąś wartość: int k(5); Liczba 5 jest tu właśnie r-wartością. W tym przypadku 5 nie jest zapisywana osobno w pamięci a później kopiowana, tylko zostaje przeniesiona do pamięci zarezerwowanej przez zmienną k. Kiedyś taka możliwość istniała jedynie w przypadku typów wbudowanych. Teraz możemy w ten sam sposób postępować z własnymi typami. Od strony technicznej jest to trochę bardziej skomplikowane.
  4. Operatory przypisania pozwala nam na skorzystanie z następującej składni: A e1; A e2 = e1; W tym przypadku zostanie wywołany operator przypisania kopiujący, ponieważ obiekt e1 istnieje już w pamięci.
  5. W przypadku tego kodu: A e1 = A(); zostanie wywołany operator przypisania przenoszący, ponieważ A() zostanie zinterpretowane jako r-wartość.

R-wartościom i przenoszeniu został poświęcony osobny artykuł.

Funkcje usunięte

Przejdźmy teraz do meritum. Jeżeli chcemy aby obiektów naszej klasy nie można było kopiować, wystarczy że sami zdefiniujemy je w ciele klasy w odpowiedni sposób. W standardach < C++11 konieczne było zefiniowanie takiego konstruktora/operatora w przestrzeni prywatnej i nie definiowanie jego ciała. Wtedy, w przypadku próby skopiowania obiektu nawet wewnątrz którejś z funkcji składowych (czyli uprawnionych do korzystania z prywatnych składowych) kompilator zwróci błąd undefined reference (konstruktor/operator będzie zadeklarowany, ale nie zdefiniowany - nie będzie czego wywołać). C++11 dostarcza nam o wiele wygodniejszego rozwiązania. Deklarację funkcji przyrównujemy do słowa kluczowego delete. Kompilator nie będzie wtedy automatycznie definiował takiej funkcji, a w przypadku próby użycia w kodzie zaprotestuje jasnym komunikatem informującym o fakcie skasowania funkcji przez twórcę klasy. Jako przykład możemy spróbować skompilować poniższy kod: Kompilator g++-4.7 (polecenie: g++ -std=c++11 delete.cpp -o delete) zwrócił następujące błędy:

1 delete.cpp: In function int main(int, char**):
2 delete.cpp:16:9: error: use of deleted function A::A(const A&)
3 delete.cpp:10:3: error: declared here
4 delete.cpp:17:9: error: use of deleted function A::A(const A&)
5 delete.cpp:10:3: error: declared here

Proszę zwrócić uwagę, że w wierszu 17 użyłem operatora przypisania kopiującego, który wywołuje konstruktor kopiujący. Usunięcie konstruktora kopiującego automatycznie uniemożliwia również korzystanie z operatora przypisania kopiującego. Analogiczna sytuacja jest w przypadku konstruktora i operatora przenoszącego. Jeżeli natomiast usunę tylko operator przypisania kopiujący to kompilator g++ skompiluje kod. Po prostu automatycznie zamieni: A e3 = e1;, na A e3(e1);.