Лекция 8

26.03.2025 Обновлено: 26.03.2025

Пишем свой MyFunction

Шаг 1. Шаблонный по типу указателя на функцию

template<typename T>
class MyFunction;

Шаг 2. Конструктор от функтора/функции

Чтобы извлечь сигнатуру вызываемого объекта, делаем специализацию шаблона по сигнатуре:

template<typename R, typename Arg>
class MyFunction<R(Arg)> {
    // ...
};

Почему нельзя сразу так? Потому что специализируется то, чего ещё не существует. Шаблон с конкретным типом в скобках — это специализация исходного шаблона.

Шаг 3. Оператор вызова

R operator()(Arg arg) {
    return R{};
}

Шаг 4. Конструктор от типов

private:
    // ...

public:
    MyFunction(TFuncPtr ptr) {}

    template<typename T>
    MyFunction(T func) {}

Как положить и лямбду, и функтор в указатель на функцию? — спрятать тип за полиморфизмом.

Type Erasure

Как спрятать TFunc (его нельзя выразить через R и Arg):

  • Делаем чисто виртуальную базовую структуру с чисто виртуальным operator().
  • Указатель на эту базу храним в поле MyFunction.
  • Внутри объявляем шаблонный класс-наследник, который хранит реальный объект (лямбду/функтор) и пробрасывает вызов.

Прикол:

  • Принимаем в конструктор объекты разного типа, у которых одинаковая семантика: они принимают такие-то аргументы и имеют operator().
  • Прячем тип создаваемого объекта.
  • Внутри создаём «обёртку», семантически работающую так же.

Две идеи:

  1. Полная специализация шаблона, которая тоже является шаблонным классом.
  2. Спрятали реальный тип за базовым полиморфным интерфейсом (type erasure).

Это и есть std::function.

Касты — обзор

  • Implicit (неявное)
  • Explicit (явное):
    • const_cast
    • static_cast
    • dynamic_cast
    • reinterpret_cast
    • C-style cast

Касты числовых типов

  • Преобразование от меньшего ранга к большему безопасно (short → int норм).
  • При одинаковом ранге, но разной знаковости — могут быть приколы со знаком.

Указатели

  • Любой указатель можно кастовать к void* и обратно.
  • Указатели одного размера можно кастовать друг в друга (но это уже опасно).

Неявные преобразования

  • 0 неявно приводится к false.
  • В for (int i = 0; i < v.size(); ++i) int сравнивается с unsigned long long → знаковый кастится к беззнаковому, может быть переполнение.

Явный C-style cast

int i = 0;
std::cout << *(double*)&i;  // считаем биты int как double — мусор
int i = 0;
int* p = &i;
double* pd = (double*)p;
double d = *pd;
  • int занимает 4 байта, double — 8. Кастуя int* к double*, мы говорим: «читай 8 байт и интерпретируй как double» — это вылет за границы корректной памяти.
  • Представление чисел тоже разное (целые двоичные vs IEEE 754 для double).
  • В функцию можно «подсунуть» указатель на другой класс — поле, которого нет, будет интерпретировано как мусор.

C-style cast — опасная штука, никак не проверяет, можем ли мы это делать.

const_cast

  • Убирает const или volatile с переменной.
  • Может работать с указателями и ссылками на одинаковые типы.
Example
  • Если объект изначально константный, изменение через const_castUB. Забота о корректности — на программисте.
  • Изменение полей в const-методе работает, только если сам объект изначально неконстантный.
  • Совместимость с C-кодом, где нет const.

static_cast

  • Пытается преобразовать через конструкторы и операторы приведения.
  • Работает в compile-time.
  • Подходит для стандартных типов.
  • Умеет приводить указатели внутри одной иерархии классов.
  • Может кастовать из void*.

Более явный и безопасный, чем C-style cast. Если каст невозможен — ошибка компиляции.

Example

Преобразование классов в одной иерархии. Если приводим Base* к Derived*, а на самом деле там Base — поля производного класса не проинициализированы, чтение даст мусор.