Klasy w C++11

Nowe elementy klas w C++11 umożliwiają:

  • Jawne definiowanie operacji specjalnych jako domyślne
  • Jawne blokowanie domyślnych operacji specjalnych
  • Blokowanie dziedziczenia po danej klasie
  • Blokowanie nadpisywania implementacji metod w klasach pochodnych
  • Upewnienie się na etapie kompilacji, że nadpisywana metoda istnieje w klasie bazowej
  • Użycie przez konstruktor innych konstruktorów klasy
  • Inicjalizowanie składowych klasy w miejscu deklaracji

Specjalne funkcje składowe klas

Specjalne funkcje składowe klas w C++11:

  • Konstruktor domyślny
  • Destruktor
  • Operacje kopiowania - konstruktor kopiujący i kopiujący operator=
  • Operacje przenoszenia - konstruktor przenoszący i przenoszący operator=

Wszystkie specjalne funkcje składowe jeżeli są domyślnie generowane przez kompilator, to posiadają następujące cechy:

  • są publiczne
  • są inline
  • są non-explicit

C++11 daje możliwość jawnego zadeklarowania funkcji specjalnych jako domyślnych lub usunięcia ich z interfejsu klasy.

Domyślne specjalne funkcje składowe - default

Deklaracja default - wymusza na kompilatorze generację domyślnej implementacji dla deklaracji specyfikowanej przez użytkownika (np. generacja domyślnego konstruktora w przypadku, gdy istnieją inne konstruktory przyjmujące parametry)

class Gadget
{
public:
    Gadget(const Gadget&); // copy constructor will prevent
                           // generating implicitly declared
                           // default ctor and move operations

    Gadget() = default;
    Gadget(Gadget&&) noexcept = default;
};

Operacje zadeklarowane jako default są traktowane jako user-declared.

W efekcie klasa:

class Any // default copy semantics enabled
{
    std::string name;
};

nie jest taka sama jak klasa zaimplementowana w poniższy sposób:

class Any  // default copy semantics deprecated in C++14 (and later probably disabled)
{
    std::string name;

    ~Any = default;
};

Usunięte funkcje składowe - delete

Deklaracja delete - usuwa wskazaną funkcję lub funkcję składową z interfejsu klasy. Nie jest generowany kod takiej funkcji, a wywołanie jej, pobranie adresu lub użycie w wyrażeniu z sizeof jest błędem kompilacji.

Aby zablokować kopiowanie obiektów danego typu w C++11 można wykorzystać następujący idiom:

// prevents object from making copies and from move operations
class NoCopyable
{
protected:
    NoCopyable() = default;

public:
    NoCopyable(const NoCopyable&) = delete;
    NoCopyable& operator=(const NoCopyable&) = delete;
};

Usuwanie funkcji z interfejsu przy pomocy słowa delete nie jest ograniczone tylko do funkcji specjalnych klas. Zastosowanie delete dla funkcji wolnej umożliwia uniknięcie niejawnej konwersji argumentów wywołania funkcji:

void integral_only(int a)
{
    cout << "integral_only: " << a << endl;
}

void integral_only(double d) = delete;

// ...

integral_only(10); // OK

short s = 3;
integral_only(s); // OK - implicit conversion to short

integral_only(3.0); // error - use of deleted function

Domyślna inicjalizacja nie-statycznych składowych klasy

W C++11 można inicjować nie-statyczne składowe klasy bezpośrednio w miejscu ich deklaracji.

Wartość użyta do inicjalizacji będzie przypisana składowej, jeśli wywoływany konstruktor nie nadpisze jej inną wartością.

Operacje kopiowania lub przenoszenia ignorują wartości domyślne

int default_id()
{
    static int id = 0;
    return ++id;
}

class Gadget
{
private:
    int id_ = default_id();
    double price_ = 0.99;
    std::string name_{"unknown"};
public:
    Gadget() = default;

    Gadget(int id) : id_ {id} {}

    Gadget(int id, double price) : id_{id}, price_ {price} {}

    Gadget(int id, double price, const std::string& name)
        : id_ {id}, price_ {price}, name_ {name}
    {}
};

// ...

Gadget a;            // a.id_ = 1, a.price_ = 0.99, a.name_ = "unknown"

Gadget b = 5;        // a.id_ = 5, a.price_ = 0.99, a.name_ = "unknown"

Gadget c {7, 2.99};  // a.id_ = 7, a.price_ = 2.99, a.name_ = "unknown"

Gadget d {9, 2.99, "item#1"};  // a.id_ = 9, a.price_ = 2.99, a.name_ = "item#1"

Ostrzeżenie

Użycie domyślnej inicjalizacji wewnątrz struktury powoduje, że przestaje ona być agregatem.

Delegowanie konstruktorów

Konstruktor może wywoływać inne konstruktory tej samej klasy.

class Item
{
private:
    int id_;
public:
    Item(int id) : id_ {id} // non-delegating constructor
    {
        // a
    }

    Item() : Item {-1} // delegating constructor
    {
        // b (called after a)
    }

    Item(const std::string& id) : Item {std::stoi(id)} // delegating constructor
    {
        // c (called after a)
    }

};

Ważne

Obiekt jest uznany za poprawnie skonstruowany, gdy pierwszy konstruktor wywołany zakończy się bez wyjątku (=> wywołany będzie destruktor obiektu).

Dziedziczenie konstruktorów

Deklaracja using może być użyta w połączeniu z konstruktorami klasy bazowej. Powoduje to niejawne deklaracje konstruktorów klasy pochodnej, które przyjmują takie same listy paramtrów co konstruktory klasy bazowej. Ich implementacja polegająca na wywołaniu wersji z klasy bazowej jest generowana tylko wtedy, gdy są one rzeczywiście użyte.

class Base
{
public:
    explicit Base(int);
    void do_something(int);
};

class Derived : public Base
{
public:
    using Base::Base; // OK in C++11
                      // implicit declaration of Derived::Derived(int)

    Derived(int, int); // overloaded inherited Base ctor
};

Użycie dziedziczenia konstruktorów w klasach pochodnych, które dodają nowe pola może być ryzykowne:

class Augmented : public Derived
{
public:
    using Derived::Derived;

private:
    std::string name_;
    int value_;
};

Augmented a {10}; // a.name_ == "" - default init
                  // and a.value_ is uninitialized

Kontrola nadpisywania metod wirtualnych - override

Słowo override ma specjalne znaczenie w deklaracji klas i powoduje sprawdzenie na etapie kompilacji, czy nadpisywana metoda jest zadeklarowana w taki sam sposób w klasie bazowej.

class Base
{
public:
    virtual void f();
    virtual void g() const;
    virtual void h(char);
    void k();
};

class Derived1 : public Base
{
public:
    void f(); // overrides Base::f()
    void g(); // doesn't override B::g() const
    virtual void h(char); // overrides B::h(char)
    void k(); // doesn't override
};

class Derived2: public Base
{
public:
    void f() override; // OK - overrides Base::f()
    void g() override; // error - doesn't override B::g() const
    virtual void h(char) override; // OK - overrides B::h(char)
    void k() override; // error - B::k() is not virtual
};

Blokowanie dziedziczenia lub nadpisywania metod - final

Od C++11 możemy zablokować dziedziczenie po klasie słowem final:

class NoInheritable final
{
    // ...
};

class Derived : public NoInheritable // error - base marked as final
{
    // ...
};

Można też określić, że implementacja metody wirtualnej jest ostateczna i nie powinna być nadpisywana:

class Base
{
public:
    virtual void f() const;
};

class Derived1 : public Base
{
public:
    void f() const override final; // enables additional optimization
};

class Derived2 : public Derived1
{
public:
    void f() const override; // error - f() attempts to override Derived1::f()
                             // marked as final
};

Statyczne składowe inline

W C++17 statyczne zmienne oznaczone jako inline są uznawane jako definicja takiej zmiennej w programie.

  • gwarantowana jest jednokrotna definicja zmiennej nawet wtedy, gdy nagłówek z definicją jest włączany w wielu jednostkach translacji
  • nie musimy tworzyć pliku cpp tylko na potrzeby definicji zmiennych globalnych/statycznych

Definicja składowej statycznej inline

  • Plik gadget.hpp
class Gadget
{
public:
    static size_t count()
    {
        return counter_;
    }
private:
    Gadget()
    {
        ++counter_;
    }

    Gadget(const Gadget&) = delete;
    Gadget& operator=(const Gadget&) = delete;

    ~Gadget()
    {
        --counter_;
    }

    static inline size_t counter_ = 0;
    static inline const std::string class_id = "Gadget";
};
  • Plik a.cpp
#include "gadget.hpp"
#include <iostream>

int main()
{
    std::cout << "No of gadgets: " << Gadget::count() << "\n";
}
  • Plik b.cpp
#include "gadget.hpp"

void bootstrap(GadgetFactory& gf)
{
    gf.register(Gadget::class_id, &make_unique<Gadget>);
}

Zmienne statyczne inline mogą być:

  • inicjalizowane przed funkcją main() lub przed pierwszym użyciem
  • mogą być thread_local
  • modyfikator constexpr implikuje, że zmienna statyczna jest inline

Przykład (plik monitor.hpp):

class Monitor
{
public:
    Monitor() { /* ... */ };

    void log(const std::string& msg);

    inline static thread_local Monitor global_monitor;
};