Pull to refresh

Comments 40

Не совсем понял как же все таки происходит управление объектами, кто отвечает за их создание/уничтожение. Во время прочтения сложилось впечатление, что мы еще на этапе проектирования программы определяем какие конкретно у нас объекты будут и знаем заранее их конкретное количество?
Как быть в случае динамического создание объектов, если нам может понадобится список из 5 объектов, а может из 105, как тут обойтись без new/delete?
В этом случае, как я писал в статье, используются контейнеры:
void Worker::Work()
{
    Worker::container.AddElement(Element(params)); //так
    Worker::container.AddElement(new Element(params)); //или на худой конец так - в зависимости от типа контейнера
}
И что, такой подход нормально работает когда нужно сделать что-то сложнее формы с гвоздями прибитыми на ней кнопками?
Очевидно да. Учитывая, с одной стороны, что на нём построен фреймворк с IDE и массой библиотек (в т.ч. связанных с GUI), а с другой — исходя из опыта разработки массы программ мною, могу точно сказать — работает отлично.
Подход предлагает более детерминированное управление ресурсами за счёт увеличения времени на проектирование иерархии классов, учитывающей время жизни ресурсов. То есть определённая альтернатива для тех, кому важно писать более стабильно работающие программы.
Честно сказать, вижу на скринах вижу нечто совершенно не гибкое и уже устаревшее аля форточки 95. Сейчас такие программы уже мало кому нужны, их и так over 9000 написано. А есть ли там продвинутый фреймворк для 2D и 3D графики?
Поддержу автора статьи, — очень даже хороший подход, и гибкости не лишает. Большие проекты тоже есть, у меня, например.
п.3 "… Время жизни объекта по указателю гарантируется самим компилятором, который на этапе компиляции проверит видимость объекта-хозяина." Поясните пожалуйста. Лучше примером кода.
Вокруг этого всё и строится. Все ограничительные пункты как раз и созданы, чтобы максимально приблизить это состояние.
О чём конкретно речь. Указатель выдаётся его хозяином. При этом, очевидно, чтобы хозяин выдал указатель, сам хозяин должен быть виден из вызываемого кода. Эта область ограничена областью видимости хозяина.
Значит, если мы «видим» хозяина, значит хозяин может выдать нам валидный указатель на своего члена. Это гарантирует сам хозяин. Потому что когда он выдаёт этот указатель, либо это указатель на обычный член-объект (и здесь вообще говорить не о чем), либо это указатель на некий объект в контейнере, создаваемый по какому-то условию — это в более сложных случаях. И тогда, опять, мы на уровне хозяина точно знаем — существует ли объект, а значит можем гарантировать факт его существования.
UFO just landed and posted this here
Конечно, предлагаемую методику можно свести к умным указателям. Но зачем? Это одна из тех вещей, которые, в общем-то, становятся не нужны.

Вообще, если честно, из меня плохой рассказчик. Мне ближе практика. Потому с удовольствием прокомментирую ваши примеры:
1. Если сокетами владеет некий класс, а сами сокеты, в конечном итоге, хранятся в его контейнере, то всё будет хорошо. А отдельным «воркерам» будут даваться ссылки/указатели на соответствующие сокеты.
При этом, при закрытии программы и вызове деструктора хозяина сокетов, все оставшиеся сокеты будут уничтожены. И всё это — без каких-либо оверхедов в рантайме, свойственных умным указателям (не забываем, что рефкаунтинг скорее всего атомарный, что ещё делает сравнение ещё более актуальным).

2. Отсутствие проблем с циклическими зависимостями — одна из основных причин, почему умные указатели могут оказаться не лучшей практикой, особенно в сравнении с более простым подходом.
Давайте вспомним: хозяин у объекта всегда один, и только один. То есть, никаких больше циклических зависимостей.

Готов обсудить ещё примеры. Это гораздо интереснее, чем теоретическая писанина.)
UFO just landed and posted this here
Спасибо за хорошие комментарии и привязку к реальным примерам.

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

Теперь давайте разберём ваш пример.
В начале вы говорите о двух потоках, потом — что сущности В используются в ряде других потоков. Этот момент не совсем понятен.
Далее, непонятно проектное решение «A должен «жить» пока живёт хоть один из его «детей» B».
То есть я не придираюсь — мне правда непонятно.

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

Судя по всему, речь идёт о реализации веб-сервера. Но зачем такие сложные счётчики ссылок и времени жизни — мне положительно не ясно (особенно учитывая, что мои серверные движки работают без них).
Предлагаю идти от задачи (здесь вам придётся чётко поставить задачу на требуемый функционал), после чего я бы на примере разобрал как можно бы было спроектировать систему так, чтобы счётчики не понадобились, то есть применить более простой подход, который представлялся в статье. Вы бы увидели подход в действии, мы могли бы обсудить конкретные плюсы и минусы предложенной реализации. Как вам такое предложение?
Возможно, я не слишком внятно выразился. В вашей постановке задачи уже содержится необходимость в проектных решениях с использованием подсчёта ссылок.
Как правило, если спроектировать чуть иначе, то их можно избежать. Поэтому я предлагаю вернуться к изначальной задаче, которую я попытаюсь реализовать немного иначе.
UFO just landed and posted this here
Любой подход не является универсальным подходом «на все случаи жизни». Это очевидно.
Я лишь хочу сказать о том, что в данной статье предлагается более простое и внятное решение целого круга задач, не использующее умные указатели со счётчиками.
И я утверждаю, что круг задач для применения подхода гораздо выше, чем принято считать. Я лишь предлагаю учиться проектировать более умно, и создавать за счёт этого более «лёгкие» системы.

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

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

Нужно говорить конкретно и обсуждать совершенно конкретные задачи. И я предлагаю это делать здесь, в комментариях. Надеясь, что это кому-то будет полезно.
UFO just landed and posted this here
Ваш третий пример, про «в колбэке дочернее окно удалено» — не совсем понятен. Поясните, пожалуйста.
UFO just landed and posted this here
Почти стал забывать, что такое ад с итераторами и энумераторами.) В U++/NTL c самых ранних версий придумали как вернуться к простым целочисленным индексам без потери функциональности и производительности. (Разумеется, это неприкрытая реклама.)

Что касается вашего примера.
Судя по всему, мы мыслим несколько разными шаблонами проектирования. То есть, мне сложно представить, что именно в реальной жизни решает тот пример, упрощённую реализацию которого вы привели.
С моей точки зрения, «нормальным» аналогом вашего примера будет вот такой код:
#include <Core/Core.h>
class window
{
public:
	window()          :parent(NULL) {}
	window(window *p) :parent(p)    {}
	
	window * create_window()
	{
		return & windows.Add();
	}
	
	bool delete_window (window *w)
	{
		for (int i=0; i<windows.GetCount(); ++i)
			if (&windows[i] == w)
			{
				windows.Remove(i);
				return true;
			}
		
		return false;
	}
	
	void destroy()
	{
		windows.Clear();
	}
	
private:
	window        *parent;
	Array<window>  windows; //контейнер с авто-очисткой
};

int _tmain(int argc, _TCHAR* argv[])
{
	window root;
	root.create_window();
	root.create_window();
	
	root.destroy();
	//в этом случае мало что изменит, потому что окна
	//внутри контейнера всё равно будут удалены в
	//деструкторе root::windows
	//в этом случае root был и остался абсолютно валидным
}

Но, видимо, это не совсем то, что вы хотели мне продемонстрировать.
UFO just landed and posted this here
Спасибо, теперь понятно. То есть, понятна данная конкретная проблема, но непонятно зачем писать так, чтобы данная проблема появлялась.

Если это проблема чисто с итератором — к дискуссии это не имеет отношения. Даже с STL контейнерами можно написать так, что это будет работать. Конечно, это будет не так элегантно, как for_each(). Кроме того, у меня такой проблемы бы не было в принципе, поскольку U++ контейнеры имеют простые целочисленные индексы (хотя итераторы тоже есть).
Да и наконец, можно сделать колбэк, возвращающий bool == true в случае, если объект был удалён. Я конечно могу ошибаться, но думаю (навскидку), что схема с простыми контейнером и чуть изменённым колбэком, имеет накладных расходов гораздо меньше, чем схема с контейнером и атомарным подсчётом ссылок в элементах. Не говоря уже о подводных камнях shared_ptr.
Если есть проблема оповещать родителя о своём удалении в деструкторе окна, то она также имеет очевидное решение: window::~window() {if (parent) parent->OnDeleted(this);}

Либо у вас есть ещё какой-то функционал, которого нет в примере, но который делает вашу схему очень привлекательной.
UFO just landed and posted this here
Ну, кто я вообще такой, чтобы говорить о том как надо. )
Собственно, и про методику эту я изначально написал, что она, наверняка, не на все случаи жизни. Дело лишь в том, что, с моей точки зрения, её правильное применение упрощает код, отладку и поддержку.

Соответственно, всё что я тут пишу, является не более чем частным мнением, которое, в общем-то, не претендует на какую-либо глобальность. Жаль только, что заметка не вызвала того интереса, который я ожидал — всё-таки, возможность привести программу к более простым и таким же эффективным решениям, это очень неплохая возможность. Для любого более или менее понимающего профессионала. Так мне кажется.

Так вот, с моей точки зрения, обход списка с его одновременной модификацией — вещь, о необходимости которой я бы сильно задумался. Особенно в тех случаях, которые приводят к такому глобальному оверхеду.
Предположим, что необходимость пройти по изменяющемуся списку всё-таки есть. Что я бы тут посоветовал… да хотя бы классический двусвязный список. В итерации прохода, сначала сохраняем указатели на текущий, предыдущий и следующий элементы. Выполняем функцию для текущего элемента. В ходе работы функции, можно уведомить родителя об уничтожении текущего объекта или добавлении новых — так, чтобы список остался валидным (как правило, никакой операции здесь производиться не будет, то есть оверхед минимальный). Теперь возвращаемся из функции в проход по списку.
Сравниваем указатель на текущий элемент и предыдущий элемент у сохранённого указателя на следующий элемент. Если они совпадают, просто сдвигаемся вперёд. Если нет — значит делаем текущий элемент равным предыдущему элементу сохранённого указателя на следующий элемент и двигаемся назад до тех пор, пока предыдущий указатель текущего элемента не будет равен сохранённому предыдущему элементу (это для случая если добавлено несколько элементов сразу; если такого быть не может — возвраты вообще не нужны). Собственно, вот и весь обход. Всего несколько строчек и минимальный оверхед.
Разумеется, если подумать подольше, можно найти более оптимальное решение. По крайней мере, более быстрое и внятное в целом по программе, чем обёртки с подсчётом ссылок.
UFO just landed and posted this here
UFO just landed and posted this here
Всё-таки, не стоит категорично утверждать, не разобравшись в предмете.

Но за комментарий спасибо: он выявил, что определённая путаница происходит и по моей вине тоже. То, что в стандарте C++0x называется move, я (в стандартах фреймворка) называю pick_. Moveable — это другое. Поскольку STL давно не пользуюсь, «проморгал» этот кусок в новом стандарте.
В этом смысле, спасибо за комментарий, буду думать как убрать путаницу в тексте.

На самом деле, упомянутый в конце статьи вариант именований мне кажется более обоснованным.
pick — это передача содержимого за счёт передачи внутренних указателей.

Теперь о moveable. Представим себе реализацию вектора, где объекты идут в обычном динамическом массиве. Чтобы добавить элемент, или вставить элемент в вектор, требуется создать массив большего размера, после чего провести копирование элементов. В крайнем случае, перенос элементов.
Но можно сделать по-другому. На самом деле, многие классы являются «простыми», в том смысле, что при обычном побайтовом копировании, не происходит ничего плохого. Это можно рассматривать как грязный хак, но он поддерживается всеми основными компиляторами самых разных версий, он прекрасно работает, и он позволяет копировать группы объектов, не вызывая даже inplace конструкторов. Да, это накладывает определённые ограничения на сам класс, но, поверьте, оно того стоит. Так вот, если фреймворку удаётся основные классы (включая контейнеры и строки) сделать ещё и moveable, это даёт серьёзный прирост скорости при управлении контейнерами.

Если мы говорим о производительности, стоит также упомянуть стратегию расширения контейнеров, более «умную» реализацию строкового класса, и ещё несколько решений.
UFO just landed and posted this here
Практика показывает, что memcpy можно использовать не только для POD. В этом всё и дело.
обычно peek это операция получения элемента без его удаления из контейнера
Такой подход очень хорош.

Но только до момента, когда у нас появляются несколько нитей.
Почему? Для многопоточной среды есть решения, не противоречащие этой методике. Об одном из них я собираюсь рассказать в следующих заметках.
Потому, что это всё работает, с таком сценарии: нить создаётся, ей передаются ссылки/указатели на данные «прародителя». Затем нить завершается, «прародитель» также ждёт завершения нити, а потом можёт удалять свои данные.

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

Вот передача владения входными данными с использованием smart_ptr'а от Сервера к нити — стандартна, минимум действий со стороны программиста, надёжна, защищена от утечек.

Как это сделать также красиво с использованием Вашего подхода — я с ходу и не представляю даже.

ps: а сам подход — очень правильный. У него, к сожалению, есть своя область применения, за пределами которой его плюсы становятся не такими заметными.
Поток при завершении работы может уведомлять класс-хозяин (путём вызова обычной функции-члена), что данные следует удалить. При этом, если нужно не просто удалить объект, а выполнить сопутствующие операции (например, обновить GUI), это всё можно сделать в той же функции. И никаких накладных расходов.
Если я правильно понял ваш пример.
Callback, в котором происходит удаление ресурса — это тот же самый ручной free/delete, от которого и хочется избавиться, так ведь? Так как у нас (с таким подходом) сразу возникает вопрос «а как гарантировать, что нить 'не забудет' вызвать этот callback?».
Если данные будем хранить в контейнере с авто-удалением (см. статью), то память не утечёт, даже если колбэк не будет вызван.

В случае, если диспетчирование не нужно, я готов согласиться с тем, что следовало бы без фанатизма по отношению к этому подходу, спокойно передать в конструктор потока указатель в обёртке вроде smart_ptr (не shared_ptr!).

К сожалению, в реальной жизни всё оказывается сложнее. Как правило, мы не готовы качать «сколько угодно» больших кусков в память за раз. Имея контейнер, мы всегда можем регулировать, сколько единиц данных и какого размера у нас открыто (и кому-то придётся постоять в очереди — это лучше чем нехватка памяти или дикий своп). Диспетчирование будет очень полезно при отладке: оно упростит отслеживание возможных утечек памяти.

Резюмирую. Ваша реализация через smart_ptr — гораздо легче shared_ptr, что делает его хорошей альтернативой моему подходу в простом случае.
То что предлагаю здесь я, не намного лучше или хуже использования умного указатель без подсчёта ссылок — в общем случае. В реальной жизни, я бы воспользовался любым из них, в зависимости от необходимости диспетчирования.
Если данные будем хранить в контейнере с авто-удалением (см. статью), то память не утечёт
Если нить завершилась, а этот кусок данных остался занимать своё место в памяти — это немногим лучше обычного memory leak. Обработал сервер 100 запросов, съел ещё 100*N мегабайт. Потом ещё 100 запросов обработал…, а потом ОС его убьёт.

в обёртке вроде smart_ptr (не shared_ptr!).
Хм… smart_ptr — это общее название всех таких указателей, shared_ptr — это тоже один из smart_ptr'ов. Я не совсем понял, что имелось в виду.
Отличный подход.
Сам использую нечто подобное, только я пошёл несколько дальше и отказался от указателей и ссылок, используя вместо этого глобальный индексы. Унаследовав все классы моей иерархии от единого предка, который при создании регистрирует себя на фабрике индексов. Когда нужно обратится к какому-то объекту, то контейнер по индексу возвращает специальный временный объект, с перегруженным оператором ->. Кроме всех вышеперечисленных преимуществ, такая идеология позволяет избежать некорректного поведения при обращении к уже удалённому объекту и очень легко реализовать многопоточность.
Sign up to leave a comment.

Articles