Files
ubbook/runtime/virtual_functions.md
Sergey Fukanchik 8c1499929a Fix a number of typos (#128)
Co-authored-by: Sergey Fukanchik <s.fukanchik@postgrespro.ru>
2025-09-29 15:13:45 +01:00

161 lines
7.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Невиртуальные виртуальные функции
Вы разрабатываете иерархию классов и хотите описать интерфейс для вычислителя, который можно запускать и останавливать.
Скорее всего в первой итерации он будет выглядеть так
```C++
class Processor {
public:
virtual ~Processor() = default;
virtual void start() = 0;
// stops execution, returns `false` if already stopped
virtual bool stop() = 0;
};
```
Пользователи интерфейса нареализовывали своих имплементаций. Все были счастливы, пока кто-то не сделал асинхронную реализацию. С ней почему-то приложение стало падать. Проведя небольшое расследование, вы выяснили, что пользователи интерфейса не позаботились вызвать метод `stop()` перед разрушением объекта. Какая досада!
Вы были уставши и злы. А быть может это были и не вы, а какой-то менее опытный коллега, которому поручили доработать интерфейс. В общем, на свет родилась правка
```C++
class Processor {
public:
virtual void start() = 0;
// stops execution, returns `false` if already stopped
virtual bool stop() = 0;
virtual ~Processor() {
stop();
}
};
```
Логично? — Да!
Правильно? — Нет!
Если вам повезет, то достаточно умный компилятор [сможет](https://godbolt.org/z/PGeob9bn1) сообщить о проблеме.
В конструкторах и деструкторах в C++ виртуальная диспетчеризация методов не работает (В других языках — например, в C# или Java — наоборот, что доставляет свои проблемы).
Почему так? При конструировании часть объекта-наследника, используемая в переопределенном методе, может быть еще не создана: конструкторы вызываются в порядке от базового класса к производному.
При деструктурировании наоборот — часть объекта-наследника уже уничтожена, и если позволить динамический вызов, можно легко получить use-after-free.
Радуйтесь! Это одно из немногих мест в C++, где вас защитили от неопределенного поведения со временами жизни!
Хорошо. А если так?
```C++
// processor.hpp
class Processor {
public:
void start();
// stops execution, returns `false` if already stopped
bool stop();
virtual ~Processor();
protected:
virtual bool stop_impl() = 0;
virtual void start_impl() = 0;
};
// processor.cpp
Processor::~Processor() {
stop();
}
bool Processor::stop() {
return stop_impl();
}
void Processor::start() {
start_impl();
}
```
Компиляторы уже [не выдают](https://godbolt.org/z/66fG6cjnd) замечательного предупреждения и подвох заметить стало сложнее. А ведь мы повысили уровень индирекции всего на один! А что будет если код нашего базового класса окажется сложнее?.. Наследование имплементаций — источник многих проблем, прячущихся за невинным желанием переиспользовать код.
Вызов виртуальных функций класса в его конструкторах и деструкторах почти всегда является ошибкой сейчас или в будущем.
Если же это не ошибка и так и задумывалось, то стоит использовать явный статический вызов с указанием имени класса (name qualified call).
```C++
// processor.cpp
Processor::~Processor() {
Processor::stop();
}
```
Также стоит отметить, что в C++ у pure virtual методов может быть имплементация, к которой можно обращаться.
Иногда это даже полезно. Таким образом можно потребовать от пользователя обязательно явно принять решение: изменять поведение метода или использовать поведение по умолчанию.
```C++
class Processor {
public:
virtual void start() = 0;
// stops execution, returns `false` if already stopped
virtual bool stop() = 0;
virtual ~Processor() = default;
};
void Processor::start() {
std::cout << "unsupported";
}
class MyProcessor : public Processor {
public:
void start() override {
// call default implementation
Processor::start();
}
};
```
Вернемся опять к нашей остановке при вызове деструктора. Как же с ней быть?
Есть два пути.
Путь первый: потребовать, чтоб реализующий интерфейс обязательно предоставил свою версию деструктора, которая выполнит корректную остановку.
Насильно, с проверкой на этапе компиляции, к этому никого, к сожалению никого не принудишь. Можно [попытаться](https://godbolt.org/z/cWrd7P89r) выразить намерение объявлением деструктора интерфейса чисто виртуальным, но это не поможет, поскольку деструктор, если не указан, всегда генерируется
```C++
class Processor {
public:
virtual void start() = 0;
// stops execution, returns `false` if already stopped
virtual bool stop() = 0;
virtual ~Processor() = 0;
};
// required!
Processor::~Processor() = default;
class MyProcessor : public Processor {
public:
void start() override {
}
bool stop() override { return false; }
// ~MyProcessor() override = default; missing destructor does not trigger CE
};
int main() {
MyProcessor p;
}
```
Путь второй — добавить еще один слой. И пользоваться им во всех публичных API
```C++
class GuardedProcessor {
std::unique_ptr<Processor> proc;
// ...
~GuardedProcessor() {
assert(proc != nullptr);
proc->stop();
}
};
```