Поток при завершении работы может уведомлять класс-хозяин (путём вызова обычной функции-члена), что данные следует удалить. При этом, если нужно не просто удалить объект, а выполнить сопутствующие операции (например, обновить GUI), это всё можно сделать в той же функции. И никаких накладных расходов.
Если я правильно понял ваш пример.
На мой взгляд, сам по себе символ имеет право на жизнь. Особенно, после некоторой «доводки».
Но, к сожалению, в начале XXI века символ будет просто нечитаем 95% населения. Время упущено.
Предлагаю добавить два варианта: Р-со-шлангом и просто «руб». Если не нравится лебедевский вариант, можно остановиться на сокращении. Но в разработке такая иконка будет полезна.
Всё-таки, не стоит категорично утверждать, не разобравшись в предмете.
Но за комментарий спасибо: он выявил, что определённая путаница происходит и по моей вине тоже. То, что в стандарте C++0x называется move, я (в стандартах фреймворка) называю pick_. Moveable — это другое. Поскольку STL давно не пользуюсь, «проморгал» этот кусок в новом стандарте.
В этом смысле, спасибо за комментарий, буду думать как убрать путаницу в тексте.
На самом деле, упомянутый в конце статьи вариант именований мне кажется более обоснованным.
pick — это передача содержимого за счёт передачи внутренних указателей.
Теперь о moveable. Представим себе реализацию вектора, где объекты идут в обычном динамическом массиве. Чтобы добавить элемент, или вставить элемент в вектор, требуется создать массив большего размера, после чего провести копирование элементов. В крайнем случае, перенос элементов.
Но можно сделать по-другому. На самом деле, многие классы являются «простыми», в том смысле, что при обычном побайтовом копировании, не происходит ничего плохого. Это можно рассматривать как грязный хак, но он поддерживается всеми основными компиляторами самых разных версий, он прекрасно работает, и он позволяет копировать группы объектов, не вызывая даже inplace конструкторов. Да, это накладывает определённые ограничения на сам класс, но, поверьте, оно того стоит. Так вот, если фреймворку удаётся основные классы (включая контейнеры и строки) сделать ещё и moveable, это даёт серьёзный прирост скорости при управлении контейнерами.
Если мы говорим о производительности, стоит также упомянуть стратегию расширения контейнеров, более «умную» реализацию строкового класса, и ещё несколько решений.
Любой подход не является универсальным подходом «на все случаи жизни». Это очевидно.
Я лишь хочу сказать о том, что в данной статье предлагается более простое и внятное решение целого круга задач, не использующее умные указатели со счётчиками.
И я утверждаю, что круг задач для применения подхода гораздо выше, чем принято считать. Я лишь предлагаю учиться проектировать более умно, и создавать за счёт этого более «лёгкие» системы.
Поскольку я практик, то предложил обсудить конкретные примеры и предложил их более эфффективную реализацию, основанную на подходе из статьи. Как, например, я предложил конкретное, более простое решение, для примера ниже. Там из-за простого обхода динамически изменяемого списка, стали городить много всего, с моей точки зрения, лишнего.
Наверняка есть какой-то класс задач, где рефкаунты просто необходимы, где они «ложатся» идеально. Но я утверждаю, что класс этих задач у'же, чем принято считать. Подозреваю, что по крайней мере некоторые из указанных вами пунктов можно реализовать «легче».
Нужно говорить конкретно и обсуждать совершенно конкретные задачи. И я предлагаю это делать здесь, в комментариях. Надеясь, что это кому-то будет полезно.
Ну, кто я вообще такой, чтобы говорить о том как надо. )
Собственно, и про методику эту я изначально написал, что она, наверняка, не на все случаи жизни. Дело лишь в том, что, с моей точки зрения, её правильное применение упрощает код, отладку и поддержку.
Соответственно, всё что я тут пишу, является не более чем частным мнением, которое, в общем-то, не претендует на какую-либо глобальность. Жаль только, что заметка не вызвала того интереса, который я ожидал — всё-таки, возможность привести программу к более простым и таким же эффективным решениям, это очень неплохая возможность. Для любого более или менее понимающего профессионала. Так мне кажется.
Так вот, с моей точки зрения, обход списка с его одновременной модификацией — вещь, о необходимости которой я бы сильно задумался. Особенно в тех случаях, которые приводят к такому глобальному оверхеду.
Предположим, что необходимость пройти по изменяющемуся списку всё-таки есть. Что я бы тут посоветовал… да хотя бы классический двусвязный список. В итерации прохода, сначала сохраняем указатели на текущий, предыдущий и следующий элементы. Выполняем функцию для текущего элемента. В ходе работы функции, можно уведомить родителя об уничтожении текущего объекта или добавлении новых — так, чтобы список остался валидным (как правило, никакой операции здесь производиться не будет, то есть оверхед минимальный). Теперь возвращаемся из функции в проход по списку.
Сравниваем указатель на текущий элемент и предыдущий элемент у сохранённого указателя на следующий элемент. Если они совпадают, просто сдвигаемся вперёд. Если нет — значит делаем текущий элемент равным предыдущему элементу сохранённого указателя на следующий элемент и двигаемся назад до тех пор, пока предыдущий указатель текущего элемента не будет равен сохранённому предыдущему элементу (это для случая если добавлено несколько элементов сразу; если такого быть не может — возвраты вообще не нужны). Собственно, вот и весь обход. Всего несколько строчек и минимальный оверхед.
Разумеется, если подумать подольше, можно найти более оптимальное решение. По крайней мере, более быстрое и внятное в целом по программе, чем обёртки с подсчётом ссылок.
Спасибо, теперь понятно. То есть, понятна данная конкретная проблема, но непонятно зачем писать так, чтобы данная проблема появлялась.
Если это проблема чисто с итератором — к дискуссии это не имеет отношения. Даже с STL контейнерами можно написать так, что это будет работать. Конечно, это будет не так элегантно, как for_each(). Кроме того, у меня такой проблемы бы не было в принципе, поскольку U++ контейнеры имеют простые целочисленные индексы (хотя итераторы тоже есть).
Да и наконец, можно сделать колбэк, возвращающий bool == true в случае, если объект был удалён. Я конечно могу ошибаться, но думаю (навскидку), что схема с простыми контейнером и чуть изменённым колбэком, имеет накладных расходов гораздо меньше, чем схема с контейнером и атомарным подсчётом ссылок в элементах. Не говоря уже о подводных камнях shared_ptr.
Если есть проблема оповещать родителя о своём удалении в деструкторе окна, то она также имеет очевидное решение: window::~window() {if (parent) parent->OnDeleted(this);}
Либо у вас есть ещё какой-то функционал, которого нет в примере, но который делает вашу схему очень привлекательной.
Увы, нет.
Насколько я знаю, галогеновые лампы есть только в магазинах стран 1-го мира. Страны третьего мира довольствуются энергосберегающими. :)
У нас такие лампы можно достать у специализированных фирм, либо в специализированных магазинах (например, попробуйте поиск Philips EcoClassic30). Наверное, можно ещё заказать в каких-то интернет-магазинах.
Почти стал забывать, что такое ад с итераторами и энумераторами.) В 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 был и остался абсолютно валидным
}
Но, видимо, это не совсем то, что вы хотели мне продемонстрировать.
Возможно, я не слишком внятно выразился. В вашей постановке задачи уже содержится необходимость в проектных решениях с использованием подсчёта ссылок.
Как правило, если спроектировать чуть иначе, то их можно избежать. Поэтому я предлагаю вернуться к изначальной задаче, которую я попытаюсь реализовать немного иначе.
Спасибо за хорошие комментарии и привязку к реальным примерам.
Вопрос «почему» ставится только в том случае, если предлагаемый подход не работает. В моём случае, это не очевидно, поэтому мой вопрос именно «зачем».
Теперь давайте разберём ваш пример.
В начале вы говорите о двух потоках, потом — что сущности В используются в ряде других потоков. Этот момент не совсем понятен.
Далее, непонятно проектное решение «A должен «жить» пока живёт хоть один из его «детей» B».
То есть я не придираюсь — мне правда непонятно.
Из того что мне удалось понять, я бы классы В клал в контейнер внутри А. Сам А при этом выдавал бы объекты В другим по запросу других потоков. О том, как гарантировать существование А в других потоках, вполне можно бы было подумать. А сам А будет гарантировать валидность указателей на В.
Если вас беспокоит, что во время использования В, его хозяин А может быть уничтожен, то это, с точки зрения данного подхода, значило бы, что В следует включать в какой-то более общий класс-хозяин.
Кстати, централизованное хранение В, может помочь в более простой диспетчеризации, или, например, ведении статистики используемых соединений, то есть сокетов, то есть, простите, В.
Судя по всему, речь идёт о реализации веб-сервера. Но зачем такие сложные счётчики ссылок и времени жизни — мне положительно не ясно (особенно учитывая, что мои серверные движки работают без них).
Предлагаю идти от задачи (здесь вам придётся чётко поставить задачу на требуемый функционал), после чего я бы на примере разобрал как можно бы было спроектировать систему так, чтобы счётчики не понадобились, то есть применить более простой подход, который представлялся в статье. Вы бы увидели подход в действии, мы могли бы обсудить конкретные плюсы и минусы предложенной реализации. Как вам такое предложение?
Конечно, предлагаемую методику можно свести к умным указателям. Но зачем? Это одна из тех вещей, которые, в общем-то, становятся не нужны.
Вообще, если честно, из меня плохой рассказчик. Мне ближе практика. Потому с удовольствием прокомментирую ваши примеры:
1. Если сокетами владеет некий класс, а сами сокеты, в конечном итоге, хранятся в его контейнере, то всё будет хорошо. А отдельным «воркерам» будут даваться ссылки/указатели на соответствующие сокеты.
При этом, при закрытии программы и вызове деструктора хозяина сокетов, все оставшиеся сокеты будут уничтожены. И всё это — без каких-либо оверхедов в рантайме, свойственных умным указателям (не забываем, что рефкаунтинг скорее всего атомарный, что ещё делает сравнение ещё более актуальным).
2. Отсутствие проблем с циклическими зависимостями — одна из основных причин, почему умные указатели могут оказаться не лучшей практикой, особенно в сравнении с более простым подходом.
Давайте вспомним: хозяин у объекта всегда один, и только один. То есть, никаких больше циклических зависимостей.
Готов обсудить ещё примеры. Это гораздо интереснее, чем теоретическая писанина.)
Вокруг этого всё и строится. Все ограничительные пункты как раз и созданы, чтобы максимально приблизить это состояние.
О чём конкретно речь. Указатель выдаётся его хозяином. При этом, очевидно, чтобы хозяин выдал указатель, сам хозяин должен быть виден из вызываемого кода. Эта область ограничена областью видимости хозяина.
Значит, если мы «видим» хозяина, значит хозяин может выдать нам валидный указатель на своего члена. Это гарантирует сам хозяин. Потому что когда он выдаёт этот указатель, либо это указатель на обычный член-объект (и здесь вообще говорить не о чем), либо это указатель на некий объект в контейнере, создаваемый по какому-то условию — это в более сложных случаях. И тогда, опять, мы на уровне хозяина точно знаем — существует ли объект, а значит можем гарантировать факт его существования.
Очевидно да. Учитывая, с одной стороны, что на нём построен фреймворк с IDE и массой библиотек (в т.ч. связанных с GUI), а с другой — исходя из опыта разработки массы программ мною, могу точно сказать — работает отлично.
Подход предлагает более детерминированное управление ресурсами за счёт увеличения времени на проектирование иерархии классов, учитывающей время жизни ресурсов. То есть определённая альтернатива для тех, кому важно писать более стабильно работающие программы.
В этом случае, как я писал в статье, используются контейнеры:
void Worker::Work()
{
Worker::container.AddElement(Element(params)); //так
Worker::container.AddElement(new Element(params)); //или на худой конец так - в зависимости от типа контейнера
}
Если я правильно понял ваш пример.
Но, к сожалению, в начале XXI века символ будет просто нечитаем 95% населения. Время упущено.
Но за комментарий спасибо: он выявил, что определённая путаница происходит и по моей вине тоже. То, что в стандарте C++0x называется move, я (в стандартах фреймворка) называю pick_. Moveable — это другое. Поскольку STL давно не пользуюсь, «проморгал» этот кусок в новом стандарте.
В этом смысле, спасибо за комментарий, буду думать как убрать путаницу в тексте.
На самом деле, упомянутый в конце статьи вариант именований мне кажется более обоснованным.
pick — это передача содержимого за счёт передачи внутренних указателей.
Теперь о moveable. Представим себе реализацию вектора, где объекты идут в обычном динамическом массиве. Чтобы добавить элемент, или вставить элемент в вектор, требуется создать массив большего размера, после чего провести копирование элементов. В крайнем случае, перенос элементов.
Но можно сделать по-другому. На самом деле, многие классы являются «простыми», в том смысле, что при обычном побайтовом копировании, не происходит ничего плохого. Это можно рассматривать как грязный хак, но он поддерживается всеми основными компиляторами самых разных версий, он прекрасно работает, и он позволяет копировать группы объектов, не вызывая даже inplace конструкторов. Да, это накладывает определённые ограничения на сам класс, но, поверьте, оно того стоит. Так вот, если фреймворку удаётся основные классы (включая контейнеры и строки) сделать ещё и moveable, это даёт серьёзный прирост скорости при управлении контейнерами.
Если мы говорим о производительности, стоит также упомянуть стратегию расширения контейнеров, более «умную» реализацию строкового класса, и ещё несколько решений.
Я лишь хочу сказать о том, что в данной статье предлагается более простое и внятное решение целого круга задач, не использующее умные указатели со счётчиками.
И я утверждаю, что круг задач для применения подхода гораздо выше, чем принято считать. Я лишь предлагаю учиться проектировать более умно, и создавать за счёт этого более «лёгкие» системы.
Поскольку я практик, то предложил обсудить конкретные примеры и предложил их более эфффективную реализацию, основанную на подходе из статьи. Как, например, я предложил конкретное, более простое решение, для примера ниже. Там из-за простого обхода динамически изменяемого списка, стали городить много всего, с моей точки зрения, лишнего.
Наверняка есть какой-то класс задач, где рефкаунты просто необходимы, где они «ложатся» идеально. Но я утверждаю, что класс этих задач у'же, чем принято считать. Подозреваю, что по крайней мере некоторые из указанных вами пунктов можно реализовать «легче».
Нужно говорить конкретно и обсуждать совершенно конкретные задачи. И я предлагаю это делать здесь, в комментариях. Надеясь, что это кому-то будет полезно.
Собственно, и про методику эту я изначально написал, что она, наверняка, не на все случаи жизни. Дело лишь в том, что, с моей точки зрения, её правильное применение упрощает код, отладку и поддержку.
Соответственно, всё что я тут пишу, является не более чем частным мнением, которое, в общем-то, не претендует на какую-либо глобальность. Жаль только, что заметка не вызвала того интереса, который я ожидал — всё-таки, возможность привести программу к более простым и таким же эффективным решениям, это очень неплохая возможность. Для любого более или менее понимающего профессионала. Так мне кажется.
Так вот, с моей точки зрения, обход списка с его одновременной модификацией — вещь, о необходимости которой я бы сильно задумался. Особенно в тех случаях, которые приводят к такому глобальному оверхеду.
Предположим, что необходимость пройти по изменяющемуся списку всё-таки есть. Что я бы тут посоветовал… да хотя бы классический двусвязный список. В итерации прохода, сначала сохраняем указатели на текущий, предыдущий и следующий элементы. Выполняем функцию для текущего элемента. В ходе работы функции, можно уведомить родителя об уничтожении текущего объекта или добавлении новых — так, чтобы список остался валидным (как правило, никакой операции здесь производиться не будет, то есть оверхед минимальный). Теперь возвращаемся из функции в проход по списку.
Сравниваем указатель на текущий элемент и предыдущий элемент у сохранённого указателя на следующий элемент. Если они совпадают, просто сдвигаемся вперёд. Если нет — значит делаем текущий элемент равным предыдущему элементу сохранённого указателя на следующий элемент и двигаемся назад до тех пор, пока предыдущий указатель текущего элемента не будет равен сохранённому предыдущему элементу (это для случая если добавлено несколько элементов сразу; если такого быть не может — возвраты вообще не нужны). Собственно, вот и весь обход. Всего несколько строчек и минимальный оверхед.
Разумеется, если подумать подольше, можно найти более оптимальное решение. По крайней мере, более быстрое и внятное в целом по программе, чем обёртки с подсчётом ссылок.
Если это проблема чисто с итератором — к дискуссии это не имеет отношения. Даже с STL контейнерами можно написать так, что это будет работать. Конечно, это будет не так элегантно, как for_each(). Кроме того, у меня такой проблемы бы не было в принципе, поскольку U++ контейнеры имеют простые целочисленные индексы (хотя итераторы тоже есть).
Да и наконец, можно сделать колбэк, возвращающий bool == true в случае, если объект был удалён. Я конечно могу ошибаться, но думаю (навскидку), что схема с простыми контейнером и чуть изменённым колбэком, имеет накладных расходов гораздо меньше, чем схема с контейнером и атомарным подсчётом ссылок в элементах. Не говоря уже о подводных камнях shared_ptr.
Если есть проблема оповещать родителя о своём удалении в деструкторе окна, то она также имеет очевидное решение: window::~window() {if (parent) parent->OnDeleted(this);}
Либо у вас есть ещё какой-то функционал, которого нет в примере, но который делает вашу схему очень привлекательной.
Насколько я знаю, галогеновые лампы есть только в магазинах стран 1-го мира. Страны третьего мира довольствуются энергосберегающими. :)
У нас такие лампы можно достать у специализированных фирм, либо в специализированных магазинах (например, попробуйте поиск Philips EcoClassic30). Наверное, можно ещё заказать в каких-то интернет-магазинах.
Что касается вашего примера.
Судя по всему, мы мыслим несколько разными шаблонами проектирования. То есть, мне сложно представить, что именно в реальной жизни решает тот пример, упрощённую реализацию которого вы привели.
С моей точки зрения, «нормальным» аналогом вашего примера будет вот такой код:
Но, видимо, это не совсем то, что вы хотели мне продемонстрировать.
Как правило, если спроектировать чуть иначе, то их можно избежать. Поэтому я предлагаю вернуться к изначальной задаче, которую я попытаюсь реализовать немного иначе.
Вопрос «почему» ставится только в том случае, если предлагаемый подход не работает. В моём случае, это не очевидно, поэтому мой вопрос именно «зачем».
Теперь давайте разберём ваш пример.
В начале вы говорите о двух потоках, потом — что сущности В используются в ряде других потоков. Этот момент не совсем понятен.
Далее, непонятно проектное решение «A должен «жить» пока живёт хоть один из его «детей» B».
То есть я не придираюсь — мне правда непонятно.
Из того что мне удалось понять, я бы классы В клал в контейнер внутри А. Сам А при этом выдавал бы объекты В другим по запросу других потоков. О том, как гарантировать существование А в других потоках, вполне можно бы было подумать. А сам А будет гарантировать валидность указателей на В.
Если вас беспокоит, что во время использования В, его хозяин А может быть уничтожен, то это, с точки зрения данного подхода, значило бы, что В следует включать в какой-то более общий класс-хозяин.
Кстати, централизованное хранение В, может помочь в более простой диспетчеризации, или, например, ведении статистики используемых соединений, то есть сокетов, то есть, простите, В.
Судя по всему, речь идёт о реализации веб-сервера. Но зачем такие сложные счётчики ссылок и времени жизни — мне положительно не ясно (особенно учитывая, что мои серверные движки работают без них).
Предлагаю идти от задачи (здесь вам придётся чётко поставить задачу на требуемый функционал), после чего я бы на примере разобрал как можно бы было спроектировать систему так, чтобы счётчики не понадобились, то есть применить более простой подход, который представлялся в статье. Вы бы увидели подход в действии, мы могли бы обсудить конкретные плюсы и минусы предложенной реализации. Как вам такое предложение?
Вообще, если честно, из меня плохой рассказчик. Мне ближе практика. Потому с удовольствием прокомментирую ваши примеры:
1. Если сокетами владеет некий класс, а сами сокеты, в конечном итоге, хранятся в его контейнере, то всё будет хорошо. А отдельным «воркерам» будут даваться ссылки/указатели на соответствующие сокеты.
При этом, при закрытии программы и вызове деструктора хозяина сокетов, все оставшиеся сокеты будут уничтожены. И всё это — без каких-либо оверхедов в рантайме, свойственных умным указателям (не забываем, что рефкаунтинг скорее всего атомарный, что ещё делает сравнение ещё более актуальным).
2. Отсутствие проблем с циклическими зависимостями — одна из основных причин, почему умные указатели могут оказаться не лучшей практикой, особенно в сравнении с более простым подходом.
Давайте вспомним: хозяин у объекта всегда один, и только один. То есть, никаких больше циклических зависимостей.
Готов обсудить ещё примеры. Это гораздо интереснее, чем теоретическая писанина.)
О чём конкретно речь. Указатель выдаётся его хозяином. При этом, очевидно, чтобы хозяин выдал указатель, сам хозяин должен быть виден из вызываемого кода. Эта область ограничена областью видимости хозяина.
Значит, если мы «видим» хозяина, значит хозяин может выдать нам валидный указатель на своего члена. Это гарантирует сам хозяин. Потому что когда он выдаёт этот указатель, либо это указатель на обычный член-объект (и здесь вообще говорить не о чем), либо это указатель на некий объект в контейнере, создаваемый по какому-то условию — это в более сложных случаях. И тогда, опять, мы на уровне хозяина точно знаем — существует ли объект, а значит можем гарантировать факт его существования.
Подход предлагает более детерминированное управление ресурсами за счёт увеличения времени на проектирование иерархии классов, учитывающей время жизни ресурсов. То есть определённая альтернатива для тех, кому важно писать более стабильно работающие программы.