Ручное управление памятью с С++ — одновременно один из самых больших плюсов и минусов в языке. Действительно, эта парадигма позволяет создавать очень производительные программы, однако она же рождает и кучу проблем. Существует несколько способов избавится от них. Я попробовал несколько и в итоге пришел к сборщику мусора. В этой статье я хочу изложить не столько реализацию сборщика мусора на С++, сколько историю идеи и его использования, каково им пользоваться и почему в итоге от него отказался.



Итак, как у большинства программистов у меня есть свой проект, а именно двумерный игровой движок. На нем и производились все «эксперименты».

Этап первый: отладка памяти


Самая часто встречающаяся проблема при ручном управлении памятью — это утечки. Чтобы узнать о них нужно следить за памятью. Именно таков и был мой первоначальных подход. Следим за созданием и удалением объектов, проверяем после завершения программы что осталось не удаленным. Делается очень просто: перегружаем операторы new и delete, чтобы они могли принимать в параметрах имя файла исходного кода и строчку, откуда происходит аллокация, и храним все аллокации в одном месте. Для удобства объявляем макрос, который и передает имя файла и строку в оператор. Соответственно при удалении объекта удаляем запись о соответствующей аллокации.

Код
#include <vector>

class MemoryManager
{
	struct AllocInfo
	{
		const char* source;
		int         line;
		int         size;
		void*       data;
	};

	static MemoryManager   instance;
	std::vector<AllocInfo> allocations;

public:
	static void RegAllocation(void* ptr, int size, const char* source, int line)
	{
		AllocInfo info;
		info.data = ptr;
		info.size = size;
		info.source = source;
		info.line = line;

		instance.allocations.push_back(info);
	}

	static void UnregAllocation(void* ptr)
	{
		for (std::vector<AllocInfo>::iterator it = instance.allocations.begin(); it != instance.allocations.end(); ++it)
		{
			if (it->data == ptr)
			{
				instance.allocations.erase(it);
				return;
			}
		}
	}

	static void PrintInfo()
	{
		for (std::vector<AllocInfo>::iterator it = instance.allocations.begin(); it != instance.allocations.end(); ++it)
			printf("0x%x: %i bytes at %s: %i", it->data, it->size, it->source, it->line);
	}
};

MemoryManager MemoryManager::instance;

void* operator new(size_t size, const char* location, int line)
{
	void* res = ::operator new(size);
	MemoryManager::RegAllocation(res, size, location, line);
	return res;
}

void  operator delete(void* allocMemory, const char* location, int line)
{
	MemoryManager::UnregAllocation(allocMemory);
}

void  operator delete(void* allocMemory)
{
	MemoryManager::UnregAllocation(allocMemory);
}

#define mnew new(__FILE__, __LINE__)

int main()
{
	int* data = mnew int;

	MemoryManager::PrintInfo();

    return 0;
}



Плюсы
  • просто и понятно
  • не затратно в плане ресурсов
  • в ходе выполнения программы мы знаем количество задействованной памяти


Минусы
  • чтобы узнать об утечках, нужно завершать программу
  • не очень информативно откуда именно происходят аллокации
  • использование перегруженных операторов немного меняет синтаксис


Этап второй: умные указатели


И правда же, самое распространенное решение проблемы управления памятью — умные указатели. О них вас обязательно спросят на собеседовании. Идея проста: вместо обычных указателей мы используем шаблонные классы, которые работают как указатели, но имеют дополнительный функционал для автоматического освобождения объектов. Вместе с объектом храним счетчик ссылок, при присваивании значения указателю увеличиваем счетчик, при уничтожении указателя — уменьшаем. Если счетчик равен нулю — объект никому не нужен и его можно освободить. Однако, есть небольшая проблема — если два объекта ссылаются друг на друга, они никогда не будут освобождены, так как у обоих счетчик ссылок равен единице.



Слабые указатели решают эту проблему зацикленности. Один из указателей делается слабым, и теперь счетчики ссылок равны нулю и оба объекта могут быть освобождены.



Что ж, давайте придумаем ситуацию посложнее.



Такую ситуацию тоже можно разрешить с помощью слабых/сильных указателей, но будет ли это легче ручного управления? Если программист что-то сделает неправильно, результат будет такой же: утекший неосвобожденный объект. На самом деле, такие ситуации встречаются редко и в целом такой подход значительно упрощает работу с памятью.

Плюсы
  • не требует много ресурсов
  • гарантирует корректное освобождение объектов при правильной архитектуре
  • просто в освоении


Минусы
  • усложняет синтаксис
  • в некоторых ситуациях сложно правильно выстроить слабые/сильные указатели
  • зацикленные ссылки приводят к утечкам


Этап третий: велосипедостроительство


Опробовав умные указатели, я остался неудовлетворен. Просто хочется использовать один у тот же тип умного указателя везде и не задумываться о зацикленных ссылках. Пусть он сам о них задумывается. Но как это сделать? Представим ситуацию:



Нужно как-то узнать что два нижних объекта зациклены и их можно удалить, ведь на них никто не ссылается. По рисунку уже несложно догадаться: если ссылки от объекта не ведут к верхнеуровневым объектам, значит он может быть освобожден. Верхнеуровневые объекты — это, грубо говоря, те объекты, с которых начинается инициализация приложения. Для С++ это объекты на стеке и статические.

Этап четвертый: сборщик мусора


Думаю, внимательный читатель уже понял, что это и есть схема работы сборщика мусора, только наоборот. Самый простой сборщик работает примерно так: спускаясь по ссылкам от верхнеуровневых объектов помечаем всех как актуальные, после этого мы имеем право удалить те, что оказались не помеченными.



Для реализации нам потребуется такой же шаблонный класс указателя, как и в случае с умными указателями. Плюс нужен сам сборщик, который будет за всем следить и производить собственно сборку мусора.

Чуть больше кода
#include <vector>
#include <map>
#include <algorithm>

class IPtr;

template<typename T>
struct Destroyer
{
	static void Destroy(void* obj)
	{ 
		(*(T*)(obj)).~T();
	}
};

class MemoryManager
{
public:
	struct ObjectInfo
	{
		void*              object;
		size_t             size;
		bool               mark;
		const char*        source;
		int                line;
		std::vector<IPtr*> pointers;

		void(*destroy)(void*) = nullptr;
	};

private:
	static MemoryManager         instance;
	std::map<void*, ObjectInfo*> objects;
	std::vector<IPtr*>           pointers;
	size_t                       allocatedBytes = 0;
	bool                         currentMark = true;

public:
	static void CollectGarbage();

protected:
	void MarkObject(ObjectInfo* info);

	friend void* operator new(size_t size, const char* source, int line);
	friend void operator delete(void* object, const char* source, int line);
	friend void operator delete(void* object);

	template<typename T>
	friend class Ptr;
};

MemoryManager MemoryManager::instance;

class IPtr
{
protected:
	void*                      object;
	MemoryManager::ObjectInfo* info;

public:
	virtual ~IPtr() {}
	virtual bool IsRoot() const = 0;

protected:
	void MarkInvalid()
	{
		object = nullptr;
		info = nullptr;
	}

	friend void operator delete(void* object);
	friend class MemoryManager;
};

template<typename T>
class Ptr: public IPtr
{
public:
	Ptr()
	{
		object = nullptr;
		info = nullptr;

		MemoryManager::instance.pointers.push_back(this);
	}

	Ptr(T* object)
	{
		this->object = object;

		auto fnd = MemoryManager::instance.objects.find(object);
		if (fnd != MemoryManager::instance.objects.end())
		{
			info = fnd->second;
			info->pointers.push_back(this);

			if (!info->destroy)
				info->destroy = &Destroyer<T>::Destroy;
		}

		MemoryManager::instance.pointers.push_back(this);
	}

	Ptr(const Ptr<T>& other)
	{
		object = other.object;
		info = other.info;

		if (info)
			info->pointers.push_back(this);

		MemoryManager::instance.pointers.push_back(this);
	}

	~Ptr()
	{
		if (info)
		{
			auto fnd = std::find(info->pointers.begin(), info->pointers.end(), this);
			if (fnd != info->pointers.end())
				info->pointers.erase(fnd);
		}

		auto fnd = std::find(MemoryManager::instance.pointers.begin(), MemoryManager::instance.pointers.end(), this);
		if (fnd != MemoryManager::instance.pointers.end())
			MemoryManager::instance.pointers.erase(fnd);
	}

	T* Get() const
	{
		return (T*)object;
	}

	bool IsValid() const
	{
		return object != nullptr;
	}

	bool IsRoot() const
	{
		return false;
	}

	operator bool()
	{
		return object != nullptr;
	}

	operator T*()
	{
		return (T*)object;
	}

	T* operator->()
	{
		return (T*)object;
	}

	T& operator*()
	{
		return *(T*)object;
	}

	const T& operator*() const
	{
		return *(T*)object;
	}

	Ptr<T>& operator=(const Ptr<T>& other)
	{
		if (info)
		{
			auto fnd = std::find(info->pointers.begin(), info->pointers.end(), this);
			if (fnd != info->pointers.end())
				info->pointers.erase(fnd);
		}

		object = other.object;
		info = other.info;

		if (info)
			info->pointers.push_back(this);

		return *this;
	}

	Ptr<T>& operator=(T* other)
	{
		if (info)
		{
			auto fnd = std::find(info->pointers.begin(), info->pointers.end(), this);
			if (fnd != info->pointers.end())
				info->pointers.erase(fnd);
		}

		object = other; 
		
		auto fnd = MemoryManager::instance.objects.find(object);
		if (fnd != MemoryManager::instance.objects.end())
		{
			info = fnd->second;
			info->pointers.push_back(this);

			if (!info->destroy)
				info->destroy = &Destroyer<T>::Destroy;
		}
		else info = nullptr;

		return *this;
	}
};

template<typename T>
class RootPtr: public Ptr<T>
{
public:
	RootPtr():Ptr() {}

	RootPtr(T* object):Ptr(object) {}

	RootPtr(const Ptr<T>& other):Ptr(other) {}

	bool IsRoot() const
	{
		return true;
	}

	operator bool()
	{
		return object != nullptr;
	}

	operator T*()
	{
		return (T*)object;
	}

	T* operator->()
	{
		return (T*)object;
	}

	T& operator*()
	{
		return *(T*)object;
	}

	const T& operator*() const
	{
		return *(T*)object;
	}

	RootPtr<T>& operator=(const Ptr<T>& other)
	{
		Ptr<T>::operator=(other);
		return *this;
	}

	RootPtr<T>& operator=(T* other)
	{
		Ptr<T>::operator=(other);
		return *this;
	}
};

void* operator new(size_t size, const char* source, int line)
{
	void* res = malloc(size);

	MemoryManager::ObjectInfo* objInfo = new MemoryManager::ObjectInfo();
	objInfo->object = res;
	objInfo->size = size;
	objInfo->mark = MemoryManager::instance.currentMark;
	objInfo->source = source;
	objInfo->line = line;

	MemoryManager::instance.objects[res] = objInfo;
	MemoryManager::instance.allocatedBytes += size;

	return res;
}

void operator delete(void* data, const char* source, int line)
{
	delete data;
}

void operator delete(void* data)
{
	auto fnd = MemoryManager::instance.objects.find(data);

	if (fnd != MemoryManager::instance.objects.end())
	{
		MemoryManager::instance.allocatedBytes -= fnd->second->size;

		for (auto ptr : fnd->second->pointers)
			ptr->MarkInvalid();

		delete fnd->second;
		MemoryManager::instance.objects.erase(fnd);
	}

	free(data);
}

void MemoryManager::CollectGarbage()
{
	instance.currentMark = !instance.currentMark;

	for (auto ptr : instance.pointers)
	{
		if (ptr->IsRoot())
		{
			if (ptr->info)
				instance.MarkObject(ptr->info);
		}
	}

	std::vector< std::map<void*, ObjectInfo*>::iterator > freeObjects;
	for (auto obj = instance.objects.begin(); obj != instance.objects.end(); ++obj)
	{
		if (obj->second->mark != instance.currentMark)
			freeObjects.push_back(obj);
	}

	for (auto obj : freeObjects)
	{
		instance.allocatedBytes -= obj->second->size;

		obj->second->destroy(obj->first);
		free(obj->first);

		for (auto ptr : obj->second->pointers)
			ptr->MarkInvalid();

		delete obj->second;
		instance.objects.erase(obj);
	}
}

void MemoryManager::MarkObject(ObjectInfo* info)
{
	info->mark = MemoryManager::instance.currentMark;

	char* left = (char*)info->object;
	char* right = left + info->size;

	for (auto ptr : instance.pointers)
	{
		char* cptr = (char*)ptr;
		if (cptr >= left && cptr < right)
		{
			if (ptr->info && ptr->info->mark != MemoryManager::instance.currentMark)
				MarkObject(ptr->info);
		}
	}
}

#define mnew new(__FILE__, __LINE__)

struct B;
struct C;
struct D;

struct A
{
	Ptr<B> pb;
	Ptr<C> pc;

	A() { printf("A()\n"); }
	~A() { printf("~A()\n"); }
};

struct B
{
	Ptr<C> pc;

	B() { printf("B()\n"); }
	~B() { printf("~B()\n"); }
};

struct C
{
	Ptr<D> pd;

	C() { printf("C()\n"); }
	~C() { printf("~C()\n"); }
};

struct D
{
	Ptr<C> pc;

	D() { printf("D()\n"); }
	~D() { printf("~D()\n"); }
};

int main()
{
	RootPtr<A> pa = mnew A;

	pa->pb = mnew B;
	pa->pc = mnew C;

	pa->pc->pd = mnew D;
	pa->pc->pd->pc = pa->pc;

	pa->pc = nullptr;

	MemoryManager::CollectGarbage();

    return 0;
}



Как это работает
Для каждого созданного объекта создается мета-информация ObjectInfo и хранится в менеджере памяти MemoryManager. Каждый такой объект создается перегруженным оператором new. ObjectInfo хранит в себе информацию о размере объекта, месте, откуда он был создан, список указателей на него и указатель на функцию для вызова деструктора этого объекта.

Для работы с объектами вместо привычных указателей используются шаблонные классы Ptr и RootPtr. RootPtr необходим для обозначения верхнеуровневых объектов, так как в ходе выполнения программы невозможно узнать что объект создан на стеке или статически (поправьте меня, если я не прав). При инициализации или копировании указателей происходит добавление и удаление указателей из соответствующих ObjectInfo.

При вызове сборки мусора меняется булевая переменная маркера на противоположную и начинается цикл отметки объектов. Цикл проходит по верхнеуровневым указателям, затем рекурсивно по их дочерним указателям. Дочерний указатель — это тот, что находится в теле объекта. После этого все объекты, у которых маркер не совпадает с текущим удаляются.


Внимательный читатель наверняка заметил еще одно — за удобство мы платим производительностью. Мы получаем все типичные минусы сборщиков мусора: излишний расход памяти и большое время работы сборки мусора. Именно на моем проекте игрового движка это фатальный минус, ведь на каждый кадр должно уходить несколько миллисекунд и просто нет времени чтобы все остановить и произвести сборку мусора. Однако, есть и хороший момент, этот сборщик мусора полностью наш и мы можем делать все что захотим!

Этап пятый: импровизация


Первое что приходит в голову — это то, что мы полностью управляем моментом сборки мусора. Совсем не обязательно делать это посреди геймплея. Можно сделать это как раз тогда, когда и происходит наибольшее количество инициализаций и уничтожений объектов, во время загрузки уровней. При разработке игр не принято делать много аллокаций/уничтожений во время активной игры. Если при каждом выстреле загружать и создавать объект пули, никакая оптимизация памяти вас не спасет. Поэтому делать долгую сборку мусора между уровнями кажется довольно живучей идеей.

Следующая идея — вовсе не вызывать сборку мусора или делать это крайне редко. Объекты по-прежнему удалять вручную или умными указателями, а сборщик мусора можно использовать в роли отладчика и подстраховки. В таком варианте мы получим производительность эквивалентную ручному управлению памятью и безопасность от утечек при сборке мусора.

Таким образом мы можем использовать сборщик по-разному. При быстром прототипировании нас не заботит производительность, но нужна стабильность, в этом случае используем автоматическую сборку. В обычной ситуации стараемся вручную управлять памятью, а сборщик нам подсказывает и подстраховывает. Ну и конечно же его можно просто выключить и перейти к полностью ручному управлению.

Помимо этого можно реализовать еще немного «вкусностей». Так как мы используем шаблонные классы-указатели, мы можем проверять их на корректность, то есть при удалении объекта вручную делать все ссылки на него невалидными. И далее в нужных местах их проверять. Еще одно из возможных улучшений — это дефрагментация памяти. На этапе сборки мусора можно перерасположить актуальные объекты в памяти для уменьшения кеш-промахов процессора, что несомненно даст прирост производительности.

Плюсы
  • защищенность от утечек и использования некорректных указателей
  • минимальные затраты при работе в полуавтоматическом режиме
  • максимальное удобство при полностью автоматическом режиме


Минусы
  • усложнение синтаксиса
  • необходимо понимание схемы работы и использования
  • процесс сборки мусора все еще занимает значительное время


Этап шестой: возврат к началу


В конце концов на решение об отказе от сборщика повлияла специфика моего проекта. Проект планируется с открытым исходным кодом и он нацелен прежде всего на удобство использования. Усложнение и без того сложного синтаксиса С++ специфичными указателями и добавление сборщика мусора несомненно плохо повлияют на проект. Просто представьте знакомство разработчика с новой технологией: ему необходимо изучать новое API да еще и с мудреной моделью управления памятью, тем более, что большинство программистов С++ и так неплохо управляются с памятью вручную.
Окончательно я убедился в возврате к ручной модели когда принял решение использовать скрипты. Именно они нужны для простоты и удобства. Делаешь прототип или простую игру — используй скрипты, экономь время и ресурсы. Необходима гибкость и производительность — добро пожаловать в С++. Тем, кто будет использовать С++ на самом деле вряд ли понадобится сборщик мусора.

Вот так я и вернулся к началу. Надеюсь мой опыт будет полезен или хотя бы интересен другим велосипедостроителям.

P.S. Код из статьи не оптимизирован и приведен лишь для понимания работы.

Комментарии (23)


  1. MrShoor
    27.04.2016 00:53
    +6

    На самом деле умные указатели достаточно легко использовать, чтобы не было циклов. Главное — придерживаться правила, что на куче сильная ссылка на указатель может быть только у одного объекта. (под кучей я имею ввиду долгоживущие объекты на куче. Если на куче выделяется память, которая гарантированно сливается при выходе из функции — то в контексте архитектуры можно рассматривать это стековой памятью, и можно хранить сильную ссылку) Все остальные ссылки на куче — должны быть слабыми. Как правило это иерархия в форме дерева (деревьев), горизонтальные связи, и связи от потомков к родителю — слабые ссылки.
    Это только кажется сложным, но на этапе разработки хорошо видно какая именно связь между объектами, и лично мне никаких удобств не доставляет.


    1. alkedr
      27.04.2016 09:41
      +1

      А ещё бывают алгоритмы, которые находят и освобождают циклы. Работают они примерно так: каждый раз когда счётчик умного указателя декрементируется, этот указатель добавляется в «список указателей, которые могут входить в циклы». Периодически из всех указателей в этом списке запускается поиск в глубину, который находит и удаляет циклы. Существует даже многопоточный вариант, который не требует остановки всех потоков.
      Описание на английском: researcher.watson.ibm.com/researcher/files/us-bacon/Bacon03Pure.pdf


    1. AndersonDunai
      27.04.2016 09:47

      Как раз таким способом и пользуюсь. После многих граблей научился создавать сильные указатели только в момент конструкции объекта, в случаях же остальных использую слабые указатели (передача объекта другому конструктору или функции, например), т.е. там, где нарушается иерархия дерева.


      1. MrShoor
        27.04.2016 11:11
        +1

        Угу, только стоит помнить что слабая ссылка имеет некоторый оверхед в виде отдельного счетчика и потенциального кешмисса. Поэтому если параметр достаточно локальный (например ходит только на стеке в текущей операции) — то сильная ссылка будет все таки быстрее.


    1. anz
      27.04.2016 11:04

      Все верно, немного простых правил и умные указатели очень хорошая вещь. Мне просто стало интересно можно ли эти правила переложить на программу, а мне, ленивому, ничего не делать.


      1. TargetSan
        27.04.2016 12:11
        +5

        Rust
        Там собсно сделали отслеживание владения и алиасинга компилятором. От зацикленных умных указателей конечно не спасает на 100%, но ногу себе отстрелить гораздо труднее.


        1. anz
          27.04.2016 12:43

          Да, мне тоже нравится их подход, но все никак времени нет опробовать.


    1. fck_r_sns
      27.04.2016 11:08
      +3

      Если сильная ссылка может быть только у одного объекта, который гарантирует, что его время жизни больше, чем у всех остальных пользователей указателя, то shared_ptr с подсчетом ссылок не нужен вообще. В этом случае у этого объекта хранится std::unique_ptr или его аналог, а у всех остальных обычный указатель.
      Согласно C++ Core Guidelines владелец указателя помечается как owner, а все остальные являются простыми указателями, для которых нельзя применять оператор delete. Проверка последнего условия выполняется с помощью статического анализа.

      shared_ptr или его аналоги нужны только в том случае, когда нет явного владельца указателя и не известно, чье время жизни будет больше.


      1. MrShoor
        27.04.2016 11:26

        Ну я поэтому и написал — придерживаться, а не строго следовать правилу. Понятное дело во всех правилах бывают исключения. Кроме того не стоит забывать, что у слабых ссылок (в отличие от голых указателей) огромный плюс в том, что нам не нужно будет чистить везде указатели когда объект умрет, что обеспечивает нам дополнительную безопасность, т.к. в случае случайного разыменования будет гарантированное обращение по null. Ну и кроме того shared нас обеспечивает дополнительной безопасностью, т.к. бывают случаи когда shared хранится только на стеке и передавать ссылку куда-то, меняя иерархию. В общем ссылки всякие нужны (в том числе и unique).


  1. lockywolf
    27.04.2016 10:29
    -1

    Почему бы не взять стандартный Hans Boehm Garbage Collector?

    http://hboehm.info/gc/


    1. anz
      27.04.2016 19:48

      У меня не было уверенности что GC мне на 100% нужен. Я даже не думал о нем, он получился когда я захотел развить идею умных указателей. Собственно все это большой эксперимент чтобы понять как оно работает и какие плюсы и минусы. Именно поэтому я не рассматривал использование готовых решений.


  1. Greendq
    27.04.2016 12:02

    Valgrind наше всё! :)


  1. apro
    27.04.2016 12:11

    Я думаю здесь стоит упоминуть garbage collection которую сделали/делают в blink (движке chrome): www.chromium.org/blink/blink-gc

    Как я понимаю в основном это решение проблемы того, что C++ объекты отображаются в язык со сборкой мусора — javascript и поэтому хорошо бы этому сборщику мусора помочь со стороный C++.


  1. lgorSL
    27.04.2016 12:13

    Вы не пробовали использовать другие стратегии сборки мусора?
    Мне кажется, необязательно останавливать весь мир каждый кадр — с помощью подсчёта ссылок можно удалять большинство объектов. Для оставшихся циклических использовать алгоритмы типа инкрементальной сборки мусора, которые позволяют делать сборку за несколько вызовов. (хотя, наверно, тогда уж проще взять язык со встроенным сборщиком мусора и не писать велосипеды)


    1. anz
      27.04.2016 12:44

      Нет, к сожалению не пробовал. Именно как раз потому что

      тогда уж проще взять язык со встроенным сборщиком мусора и не писать велосипеды


  1. lpre
    27.04.2016 14:12

    Я бы добавил «Этап 0» (он же «Этап 7»): как можно чаще использовать RAII (т.е. автоматическое выделение памяти) вместо динамического выделения.

    Хотя полностью избавиться от динамического выделения удается редко, однако к этому следует стремиться, т.к. RAII по сути — это compile-time «garbage collector», простой, надежный, быстрый и масштабируемый.


    1. snizovtsev
      27.04.2016 15:15

      Этапом 0 должен быть анализ требований и подбор технологий под них. Если понадобился GC в C++, то проблема скорее всего в выборе технологий (C++) или персонале (который плохо знает C++).


    1. mt_
      27.04.2016 19:35

      Полностью поддерживаю. Вопрос в использовании подхода на сложных динамических сценариях, но и здесь есть решения. Откровенно говоря, за всё время ещё ни разу не встречал в С++ необходимости использовать мусоросборщик в масштабах программы (исключая специализированные классы, внутри которых та или иная сборка «мусора» может быть уместна).


  1. mt_
    27.04.2016 19:33

    Мне кажется, что в комментариях к статье будет вполне адекватно упомянуть и о подходе, который позволяет избегать проблем с памятью без мусоросборщика. Не претендуя на «метод на все случаи жизни», очень надеюсь, он будет полезен некоторым. Речь идёт об этой хабрастатье.


  1. izvolov
    28.04.2016 12:00

    Я бы немного переписал статью.
    Начал бы так же: «Ручное управление памятью в С++...».
    А всё, что идёт дальше, я бы выбросил и написал «не используется».
    В теги запишем «RAII, деструктор, умный указатель».
    Будет гораздо интереснее и познавательнее.

    И не надо придумывать про огромное количество циклов в графе зависимостей объектов. Подобные ситуации — следствие излишней увлечённостью ООП и неправильного проектирования (либо полное его отсутствие).

    Если уж пишешь на плюсах, то пиши на плюсах, а не на сях, явах или каких-то других языках.

    Также рекомендую почитать статью «Пять популярных мифов о языке C++» и поискать в интернете информацию по ключевым словам «GSL» и «owner<T*>».


    1. anz
      28.04.2016 13:09

      Возможно, вы немного не поняли статью. Она не о том, что «давайте все станем ленивыми и будем использовать воот такууую фигню». Она как раз о

      Если уж пишешь на плюсах, то пиши на плюсах, а не на сях, явах или каких-то других языках.


      В проекте GC был сделан не столько для реального использования, сколько для того чтобы попробовать каков он в деле, и узнать возможно ли его частичное применение в проекте. Сейчас его нет в проекте, и причины этого описаны в конце статьи.


  1. Light241
    28.04.2016 13:54

    копеечка про игровые движки: в unreal engine плюсы как раз с gc идут.


  1. babylon
    29.04.2016 09:43

    А в умных указателях есть подписка на объекты и события, связанные с освобождением, захватом и использованием объекта? Понятно, что у ownera самый высокий приоритет.