<aside> 🌟 В этой практике мы познакомимся с идиомой RAII и реализуем свои умные указатели.

</aside>

Шаблон задачи в replit

RAII + Templates

Получение ресурса есть инициализация (англ. Resource Acquisition Is Initialization (RAII)) — программная идиома объектно-ориентированного программирования, смысл которой заключается в том, что с помощью тех или иных программных механизмов получение некоторого ресурса неразрывно совмещается с инициализацией, а освобождение — с уничтожением объекта.

😨 Что это было?

Разберёмся, что же имеется ввиду.

Если вы открыли файл, то вам надо его закрыть. Так написаны библиотеки. Так работает ОС. Так устроен мир. Давайте напишем код для обработки мемасов:

void my_cool_function() {
	auto file = open("funny_meme.jpg"); // тут нам выдают какой-то ресурс
	// делаем всякие штуки тут
	// просто кодим
	// окда
	close(file); // а тут мы ресур "возвращаем". Как очистка памяти.
	// теперь всё ок, память не утекла, ОС не лагает, 
	// юзер счастлив и может открыть еще одну вкладку браузера
}

Почему бы не писать всегда так? В чём проблема?

  1. Во-первых, можно забыть написать close(file). И я бы точно забыл. 🤪

  2. Во-вторых, может быть непонятно, надо освобождать или нет. Когда освобождать? Вдруг через 10 минут и в другой функции? Или как-то вместе с чем-то еще? Или внутри вложенного if-else? 🤯

  3. В-третьих, если произойдет какая-то ошибка в строчке // окда и выпадет исключение (вы же помните что это?), тогда ресурс окажется неосвобождённым, а это неэтично 😵. Принцип RAII и некоторые механизмы языка позволят нам ГАРАНТИРОВАННО освободить ресурс. Но как?

    // !!!!!!
    // Пример кода для __гиков__.
    
    // lazy_string - это какой-то класс для ленивых строк, ок.
    // lhs - left hand side
    // rhs - right hand side
    lazy_string concat(lazy_string lhs, lazy_string rhs) {
        // тут мы выделяем память вручную
        // Забавно, но этот buffer утечет, если кто-то бросит исключение
        char* buffer = new char*[lhs.size() + rhs.size() + 1]{};
    
        for (size_t i = 0; i < lhs.size(); ++i) {
            buffer[i] = lhs.at(i);
        }
        // не по тому итерируемся, at бросит исключение
        for (size_t i = 0; i < lhs.size(); ++i) {
            buffer[lhs.size() + i] = rhs.at(i); // БДЫЩЬ!!!!!!!
        }
        auto result = lazy_string(buffer);
    		// там вылетело исключение, а сюда мы больше НИКОГДА не дойдем
        // значит delete не будет вызван. Значит память утекла
        delete[] buffer;
        return result;
    }
    

🧐 Как сделать RAII в С++?

В с++ есть классы. У классов есть конструкторы и деструкторы. Сконструировать экземпляр класса можно на стеке, а можно в куче. Переменные, созданные на стеке называются "автоматическими". А автоматические переменные удаляются, хм, автоматически, когда заканчивается область их действия (закончился блок кода внутри фигурных скобок, выпало исключение, выключили интернет в этой стране). А когда экземпляр уничтожается, то вызывается деструктор (или вызывается деструктор, чтобы объект уничтожился? weird 😓).

Чувствуете?

За нас кто-то автоматически вызовет функцию-деструктор, даже если вылетит исключение!!! Этот эффект мы и будем эксплуатировать. 🙀

Память как ресурс

Память в куче - это тоже ресурс. Мы получаем этот ресурс вызывая new или malloc. И возвращаем с помощью delete или free. И мы можем забыть вызвать освобождение или освобождение может быть отложенным, непонятным или сломаным из-за исключения.

<aside> 🌟 Давайте сделаем умные указатели

</aside>

Я могу написать очень простой код для "умного указателя". Который будет использовать идиому RAII и автоматически очистит память!