Пишем свой 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(). - Прячем тип создаваемого объекта.
- Внутри создаём «обёртку», семантически работающую так же.
Две идеи:
- Полная специализация шаблона, которая тоже является шаблонным классом.
- Спрятали реальный тип за базовым полиморфным интерфейсом (type erasure).
Это и есть std::function.
Касты — обзор
- Implicit (неявное)
- Explicit (явное):
const_caststatic_castdynamic_castreinterpret_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с переменной. - Может работать с указателями и ссылками на одинаковые типы.
- Если объект изначально константный, изменение через
const_cast— UB. Забота о корректности — на программисте. - Изменение полей в
const-методе работает, только если сам объект изначально неконстантный. - Совместимость с C-кодом, где нет
const.
static_cast
- Пытается преобразовать через конструкторы и операторы приведения.
- Работает в compile-time.
- Подходит для стандартных типов.
- Умеет приводить указатели внутри одной иерархии классов.
- Может кастовать из
void*.
Более явный и безопасный, чем C-style cast. Если каст невозможен — ошибка компиляции.
Преобразование классов в одной иерархии.
Если приводим Base* к Derived*, а на самом деле там Base — поля производного класса не проинициализированы, чтение даст мусор.