Стирание типа

Материал из Википедии — свободной энциклопедии
Перейти к навигации Перейти к поиску

Стирание типа (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().

Примечания

[править | править код]
  1. C++ type erasure - C++ Articles. Дата обращения: 14 сентября 2023. Архивировано 8 февраля 2023 года.
  2. Chapter 8. Classes. Дата обращения: 14 сентября 2023. Архивировано 19 сентября 2023 года.
  3. 1 2 Идиомы С++. Type erasure / Хабр. Дата обращения: 14 сентября 2023. Архивировано 21 апреля 2023 года.