СтатьиВведение в C++11: новые спецификаторы

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

Сегодня я опять продолжу тему нового стандарта 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.

Комментарии (4):
@Ksenia

видимо, автор под словом "перегрузка" метода имеет в виду "переопределение". перегрузка имеет отношение к поддержке разных типов параметров у методов с одинаковым названием, а переопределение - к механизму виртуальных функций.

> ответить
@ikalnitsky

@Ksenia, да, так лучше. Спасибо. :) Просто я обычно использую английскую терминологию, а там это overriding. Несколько не так перевел на Великий и Могучий.

> ответить
@Тоха

Насчет override: Но ведь мы можем в производном классе написать: "using Base::doSomething;" и после этого смело перегружать в производном эту функцию как захотим, поправьте, если я не прав

> ответить
@ikalnitsky

@Тоха, верно, но суть не в этом. Суть в том, что using Base::doSomething; решит проблему перекрытия. То есть ситуация следующая. У нас есть базовый класс со следующей сигнатурой метода doSomething:

class Base
{
public:
    virtual void doSomething(const char* filename);
};

А теперь мы определяем класс наследник и хотим переопределить поведение doSomethings, но из-за своей невнимательности поставили другой тип аргумента (вместое const char*const wchar_t*).

class Derived : public Base
{
public:
    virtual void doSomething(const wchar_t* filename);
};

В результате произошло перекрытие метода. Что такое перекрытие? Это значит, что класс Derived реально содержит два метода:

virtual void doSomething(const char* filename);
virtual void doSomething(const wchar_t* filename);

Но первый (который унаследован от Base) может использоваться только в методах самого класса, а извне не доступен. То есть ситуация следующая:

Derived d;
d.doSomething("some.txt"); // ошибка!
d.doSomething(L"some.txt"); // ok!

Прописав же в классе Derived строку using Base::doSomething; мы откроем доступ к унаследованному из класса Base методу, и методы будут перегруженными.

Но это никоим образом не касается override. Суть в том, что из-за перекрытия реально сущесвуют два метода и поэтому механизм виртуальности не сработает — так как это два разных метода, с разной сигнатурой. Вот как раз override и служит для того, чтобы защитить от случайного объявления нового метода, вместо переопределения. То есть, добавив к сигнатуре метода doSomething класса Derived спецификатор override:

class Derived : public Base
{
public:
    virtual void doSomething(const wchar_t* filename) override;
};

компилятор этот код не скомпилирует. Он выдаст ошибку о том, что метод

virtual void doSomething(const wchar_t* filename);

в базовом классе не найден. То есть, override как бы проверяет: "а действительно ли мы переопределяем метод?".

> ответить



Comment system uses reStructuredText. [show/hide tip]