Новые спецификаторы в C++11

Ну вот и закончились новогодние праздники. Жизнь постепенно возвращается в привычное русло и вместе с тем я продолжаю писать в блог.

Сегодня я опять продолжу тему нового стандарта C++ и расскажу о некоторых нововведениях, которые будут весьма полезны разработчикам классов. Речь пойдет о спецификаторах, предоставленных С++11. А именно: override, final, default и delete.

Спецификатор override

Понятие переопределения, думаю, известно всем. Здесь я не буду описывать что это и как проявляется. Я лишь отмечу об одной проблеме (или, скорее, особенности) старого стандарта касательно переопределения функций в классе наследнике.

Рассмотрим следующую ситуацию.

class Base
{
public:
    virtual void doSomething(int x);
};
// ...
class Derived : public Base
{
public:
    virtual void doSomething(long x);
};

Что мы имеем? У нас есть некоторый базовый класс и класс-наследник. Допустим, пользователь захотел изменить в классе-наследнике поведение метода doSomething(). Он его переопределяет, но по некой причине (невнимательности, например) нечаянно указал другой тип аргумента: long вместо int. Компилятор на это не ругнется, но код не будет работать так, как это запланировал автор класса.

Все дело в том, что методы обладают различными сигнатурами и, в данном случае, произойдет перекрытие методов. Перекрытие — это отдельная тема. Сейчас я лишь отмечу, что работая через указатель/ссылку на базовый класс, будет вызываться метод определенный в базовом классе, но никак не метод, переопределенный нами.

Необходимо будет потратить определенное время, чтобы отыскать ошибку. Новый же стандарт C++ вводит ключевое слово override, которое позволяет отслеживать подобного рода ошибки и переводить их на ошибки времени компиляции.

class Base
{
public:
    virtual void doSomething(int x);
};
// ...
class Derived : public Base
{
public:
    virtual void doSomething(long x) override;
};

Иными словами, компилятор, обнаружив override, проверяет существование метода с данной сигнатурой в базовом классе. Если же такого метода нет — выдает ошибку.

Спецификатор final

С++11 позволяет запрещать в классах-наследниках переопределение определенных методов. Достигается это за счет применения спецификатора final рядом с сигнатурой метода.

class Base
{
public:
    virtual void doSomething(int x) final;
};
// ...
class Derived : public Base
{
public:
    virtual void doSomething(int x); // ошибка!
};

Данный спецификатор также позволяет запрещать наследование от некоторого класса.

class Base final {};
class Derived : public Base {}; // ошибка!

Спецификатор final издавна существует в Java. Наконец он появился и в C++.

Спецификатор default

Полезность данного спецификатора весьма спорная: кто-то найдет его полезным, а кому-то он покажется бесполезным. Так или иначе, суть его заключается в том, что пользователь может указать компилятору реализовать ту или иную функцию-член класса по-умолчанию. Что имеется ввиду? Предположим есть класс:

class Foo
{
public:
    Foo(int x) {/* ... */}
};

Как видно, класс имеет один пользовательский конструктор, а значит конструктор по-умолчанию сгенерирован не будет. Дабы стала возможна запись вида:

Foo obj;

пользователю необходимо определить конструктор без параметров.

class Foo
{
public:
    Foo() {}
    Foo(int x) {/* ... */}
};

Вместо определения конструктора без параметров, в C++11 появилась возможность просто указать компилятору сгенерировать его по-умолчанию. Достигается это, как я сказал выше, при помощи спецификатора default.

class Foo
{
public:
    Foo() = default;
    Foo(int x) {/* ... */}
};

Реализация по-умолчанию более эффективна, чем реализация определенная пользователем. Но при нынешних системах, я не думаю что затраты на пользовательский конструктор буду заметны. В любом случае, об этом спецификаторе стоит знать.

Стоит отметить, что он применим только к специальным функциям-членам. К специальным относятся:

  • конструктор по-умолчанию;
  • конструктор копий;
  • конструктор перемещения (введен в C++11);
  • оператор присваивания;
  • оператор перемещения (введен в C++11);
  • деструктор.

Спецификатор delete

Данный спецификатор более полезный, нежели спецификатор default. Он призван пометить те методы, работать с которыми нельзя. То есть, если программа ссылается явно или неявно на эту функцию — ошибка на этапе компиляции. Запрещается даже создавать указатели на такие функции.

class Foo
{
public:
    void baz() = delete;
};

С помощью этого спецификатора, можно легко запретить конструктор копий (который уже все привыкли прятать в private) или запретить автоматическое приведение типов.

class Foo
{
public:
    Foo() = default;
    Foo(const Foo&) = delete;
    void bar(int) = delete;
    void bar(double) {}
};
// ...
Foo obj;
obj.bar(5);     // ошибка!
obj.bar(5.42);  // ok

Можно также запретить оператор new:

class Foo
{
public:
    void *operator new(std::size_t) = delete;
    void *operator new[](std::size_t) = delete;
};
// ...
Foo* ptr = new Foo; // ошибка!

Заключение

К сожалению, все рассмотренные спецификаторы кроме default и delete поддерживаются начиная с g++-4.7, который мне так и не удалось найти под Ubuntu. Поэтому override и final тестировались на компиляторе clang++ 2.9.