Статьи → Введение в 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.
видимо, автор под словом "перегрузка" метода имеет в виду "переопределение". перегрузка имеет отношение к поддержке разных типов параметров у методов с одинаковым названием, а переопределение - к механизму виртуальных функций.
@Ksenia, да, так лучше. Спасибо. :) Просто я обычно использую английскую терминологию, а там это overriding. Несколько не так перевел на Великий и Могучий.
Насчет override: Но ведь мы можем в производном классе написать: "using Base::doSomething;" и после этого смело перегружать в производном эту функцию как захотим, поправьте, если я не прав
@Тоха, верно, но суть не в этом. Суть в том, что using Base::doSomething; решит проблему перекрытия. То есть ситуация следующая. У нас есть базовый класс со следующей сигнатурой метода doSomething:
А теперь мы определяем класс наследник и хотим переопределить поведение doSomethings, но из-за своей невнимательности поставили другой тип аргумента (вместое const char* — const wchar_t*).
В результате произошло перекрытие метода. Что такое перекрытие? Это значит, что класс Derived реально содержит два метода:
Но первый (который унаследован от Base) может использоваться только в методах самого класса, а извне не доступен. То есть ситуация следующая:
Прописав же в классе Derived строку using Base::doSomething; мы откроем доступ к унаследованному из класса Base методу, и методы будут перегруженными.
Но это никоим образом не касается override. Суть в том, что из-за перекрытия реально сущесвуют два метода и поэтому механизм виртуальности не сработает — так как это два разных метода, с разной сигнатурой. Вот как раз override и служит для того, чтобы защитить от случайного объявления нового метода, вместо переопределения. То есть, добавив к сигнатуре метода doSomething класса Derived спецификатор override:
компилятор этот код не скомпилирует. Он выдаст ошибку о том, что метод
в базовом классе не найден. То есть, override как бы проверяет: "а действительно ли мы переопределяем метод?".