Стандарт C++11 определяет новое ключевое слово override
, которое, хотя и не обязательно к применению, тем не менее является значительным улучшением языка. Взглянем на следующий код:
#include <iostream> #include <memory> class Fruit { public: Fruit() = default; virtual int size() { std::cout << __PRETTY_FUNCTION__ << std::endl; } virtual double weight() const { std::cout << __PRETTY_FUNCTION__ << std::endl; } virtual int color(const int& c) { std::cout << __PRETTY_FUNCTION__ << std::endl; return c; } virtual double volume(const double& v) { std::cout << __PRETTY_FUNCTION__ << std::endl; return v; } }; class Orange : public Fruit { public: Orange() = default; virtual int size() const override { std::cout << __PRETTY_FUNCTION__ << std::endl; } virtual double weight() override { std::cout << __PRETTY_FUNCTION__ << std::endl; } virtual int color(const int c) const override { std::cout << __PRETTY_FUNCTION__ << std::endl; return c; } virtual double volume(const double&& v) override { std::cout << __PRETTY_FUNCTION__ << std::endl; return v; } }; int main() { std::unique_ptr<Fruit> f = std::make_unique<Orange>(); f->size(); f->weight(); f->color(32); f->volume(1.23); return 0; };
Если мы ожидаем, что вывод будет таким:
virtual int Orange::size() virtual double Orange::weight() const virtual int Orange::color(const int&) virtual double Orange::volume(const double&)
то сильно ошибемся, поскольку это показывает нам лишь то, что мы хотим вместо того, что происходит на самом деле. Проблема заключается в том, что ни один из вышеперечисленных методов не был переопределен.
В действительности вывод будет следующим:
virtual int Fruit::size() virtual double Fruit::weight() const virtual int Fruit::color(const int&) virtual double Fruit::volume(const double&)
При правильном подходе к разработке подобные ошибки обычно отлавливаются при модульном тестировании. Но теперь язык облегчает нам жизнь, выдавая их на этапе компиляции. Давайте добавим ключевое слово override
в каждый метод наследуемого класса:
class Orange : public Fruit { public: Orange() = default; virtual int size() const override { std::cout << __PRETTY_FUNCTION__ << std::endl; } virtual double weight() override { std::cout << __PRETTY_FUNCTION__ << std::endl; } virtual int color(const int c) const override { std::cout << __PRETTY_FUNCTION__ << std::endl; return c; } virtual double volume(const double&& v) override { std::cout << __PRETTY_FUNCTION__ << std::endl; return v; } };
Теперь при компиляции мы получим следующее:
01_override.cpp:32:15: error: ‘virtual int Orange::size() const’ marked ‘override’, but does not override virtual int size() const override ^ 01_override.cpp:36:18: error: ‘virtual double Orange::weight()’ marked ‘override’, but does not override virtual double weight() override ^ 01_override.cpp:40:15: error: ‘virtual int Orange::color(int) const’ marked ‘override’, but does not override virtual int color(const int c) const override ^ 01_override.cpp:45:18: error: ‘virtual double Orange::volume(const double&&)’ marked ‘override’, but does not override virtual double volume(const double&& v) override ^
Подобные ситуации часто возникают на ранних этапах разработки, когда базовые классы имеют нестабильный интерфейс из-за частых изменений сигнатур их методов.
Рассмотрим еще один случай, когда override
поможет сэкономить кучу времени и нервов:
#include <iostream> #include <memory> class Data { public: Data() = default; ~Data() { std::cout << __PRETTY_FUNCTION__ << std::endl; }; }; class Fruit { public: Fruit() = default; ~Fruit() { std::cout << __PRETTY_FUNCTION__ << std::endl; }; }; class Orange : public Fruit { public: Orange() : data(new Data) { } virtual ~Orange() { std::cout << __PRETTY_FUNCTION__ << std::endl; delete data; }; private: Data* data; }; int main() { std::unique_ptr<Fruit> p = std::make_unique<Orange>(); return 0; };
Опять же, мы можем ожидать, что результат будет таким:
virtual Orange::~Orange() Data::~Data() virtual Fruit::~Fruit()
Однако, если соберем и запустим этот код, то получим:
Base::~Base()
Никакого вывода ни из деструктора наследника, ни тем более из деструктора динамически выделенного объекта!
Как можно легко догадаться, в данной ситуации у нас будет утечка памяти, поскольку в конструкторе наследника была выделена память для объекта класса Data
, а деструктор базового класса Fruit
не помечен как virtual
.
Данную ситуацию можно легко исправить, пометив деструктор наследуемого класса соответствующим образом:
virtual ~Orange() virtual { std::cout << __PRETTY_FUNCTION__ << std::endl; delete data; };
В результате при компиляции получим ошибку:
02_override.cpp:30:11: error: ‘virtual Orange::~Orange()’ marked ‘override’, but does not override virtual ~Orange() override ^
Итак, из всего выше сказанного можно сделать следующий вывод: если при переопределении методов и деструкторов наследуемых классов всегда использовать ключевое слово override
, то можно оградить себя от лишней головной боли при поиске утечек памяти и вызова не тех методов.
Свежие комментарии