Стирание типа
Стирание типа (type erasure) — идиома программирования, один из способов корректно и единообразно обрабатывать данные разного типа. А именно: переменная конкретного типа начинает храниться в ячейке памяти общего типа, без всяких меток типа, и обработка проводится в общем типе, если это можно. На более высоком уровне вычисление планируется так, что преобразование типов «вниз», из общего в конкретный, всегда оказывается корректным.
Некоторые источники считают, что виртуальный полиморфизм — это уже стирание типа[1].
Ключевой недостаток стирания типов — нельзя надёжно, одним просмотром памяти, узнать при выполнении, какой тип был стёрт. Преимущество стирания типов в компактности.
Примеры
[править | править код]Большинство машинных кодов
[править | править код]Как правило, команды процессора имеют очень бедный набор доступных типов: целый байт (2, 4, 8, 16 байтов; какой-то из этих целых типов заодно и указатель), дробные 4 и 8 байтов, иногда (SIMD или 3D) небольшие векторы и матрицы из этих типов.
Поверх всего этого языки высокого уровня выстраивают сложную систему типов, и компилятор сам планирует вычисление так, что если, например, загружаются данные из 4-байтовой дробной переменной, то обязательно будут вызываться машинные команды, применимые к 4-байтовому дробному.
Шаблонное программирование в Java работает так. Когда пишут
class Wrapper<T> {
T value;
}
создаётся всего один скомпилированный класс (Wrapper.class
), и в нём переменная value
типа Object
. Но компилятор своими силами налаживает работу так, чтобы Wrapper<Integer>
не пересекался с Wrapper<String>
, и преобразования типов получаются верными.
Именно поэтому запрещается[2] шаблонно наследоваться от Throwable
, прямо или косвенно — ведь непонятно, какой из блоков вызвать в этом коде:
try {
throw new GenericException<Integer>();
}
catch (GenericException<Integer> e) {
System.err.println("Integer");
}
catch (GenericException<String> e) {
System.err.println("String");
}
Другой недопустимый код в Java, именно из-за стирания типа — вызов неизвестного заранее конструктора.
<T> T instantiateElementType(List<T> arg) {
return new T(); // Ошибка
}
В Си++ шаблонное программирование не стирает тип, а генерирует для каждой специализации свой машинный код, и аналогичная программа вполне возможна.
template <class T>
auto instantiateElementType(std::span<const T> arg) {
return std::make_unique<T>();
}
Создание виртуального полиморфизма там, где его не было…
[править | править код]…Или добавление второй, чисто серверной, таблицы виртуальных методов в шаблон «команда».
Предположим, что есть разные команды — например, «напечатать число» и «напечатать строку». Первая таблица виртуальных методов общая для клиента и сервера, и отвечает за «лёгкие» вещи вроде сериализации и записи в журнал. Только сервер умеет исполнять команды, и для этого нужна отдельная вторая таблица виртуальных методов.
Здесь стирание типа — это хранение команды в unique_ptr<Concept>
, а не в unique_ptr<Model<T>>
[3]. Таблица виртуальных методов может быть и первая — для единообразной обработки совершенно не связанных типов; так работает тип any
Си++17[3].
#include <iostream>
#include <memory>
#include <vector>
/// Общее для клиента и сервера
class ClientCommand {
public:
virtual ~ClientCommand() = default;
// функции write(), log() и прочие, нам сейчас не нужные
};
class PrintInt : public ClientCommand {
public:
int value;
PrintInt(int v) : value(v) {}
};
class PrintString : public ClientCommand {
public:
std::string value;
PrintString(std::string v) : value(std::move(v)) {}
};
/// Сервер
// Исполнение команд редко бывает шаблонным — сделаем нешаблонно…
void doExec(const PrintInt& x) { std::cout << x.value << std::endl; }
void doExec(const PrintString& x) { std::cout << x.value << std::endl; }
class ServerCommand {
public:
void exec() const { value->exec(); }
template <class Clicmd, class... Args>
static ServerCommand make(Args&&... args)
{ return ServerCommand(new Model<Clicmd>(std::forward<Args>(args)...)); }
private:
class Concept {
public:
virtual ~Concept() = default;
virtual void exec() const = 0;
};
template <class Clicmd> class Model : public Concept {
public:
static_assert(std::is_base_of_v<ClientCommand, Clicmd>);
Clicmd value;
template <class... Args> Model(Args&&... args)
: value(std::forward<Args>(args)...) {}
void exec() const override { doExec(value); }
};
std::unique_ptr<Concept> value;
ServerCommand(Concept* v) : value(v) {}
};
int main()
{
std::vector<ServerCommand> commands;
commands.push_back(ServerCommand::make<PrintInt>(42));
commands.push_back(ServerCommand::make<PrintString>("Wikipedia"));
for (auto& cmd : commands) {
cmd.exec();
}
}
Обработчики событий Embarcadero Delphi и Qt
[править | править код]Переменная «кто послал событие» обычно имеет самый общий тип. Например, для кнопки задаём такое событие:
procedure MyButtonClick(Sender : TObject);
begin
with TButton(Sender) do ...;
end;
По канонам Delphi параметр Sender
имеет самый общий тип TObject
. Но мы знаем, что событие применено к кнопке, и соответственно преобразуем тип. Аналогичным образом работает QObject::sender()
.
Примечания
[править | править код]- ↑ C++ type erasure - C++ Articles . Дата обращения: 14 сентября 2023. Архивировано 8 февраля 2023 года.
- ↑ Chapter 8. Classes . Дата обращения: 14 сентября 2023. Архивировано 19 сентября 2023 года.
- ↑ 1 2 Идиомы С++. Type erasure / Хабр . Дата обращения: 14 сентября 2023. Архивировано 21 апреля 2023 года.