Здравствуйте, Хвост, Вы писали:
Х>увидел static и дальше было лениво смотреть.
а что не так со static? почему он лень навивает? он только в базовом классе — где подсчет ссылок идет, а дальше более-менее стандартный template-ный смартпоинтер.
Здравствуйте, makes, Вы писали:
Х>>увидел static и дальше было лениво смотреть. M>а что не так со static? почему он лень навивает? он только в базовом классе — где подсчет ссылок идет, а дальше более-менее стандартный template-ный смартпоинтер.
Потому что это thread-unsafe синглетон Мейерса. Да ещё и с ошибками.
Перекуём баги на фичи!
Re[2]: между прочим boost::shared_ptr thread-safe'ный
// порочная идея - хранить счётчики в общей помойке.
// во-первых, каждая операция - O(logN)
// во-вторых, бутылочное горлышко (невозможность двум независимым потокам параллельно работать)
M> static int increase_refcount(const void *pt)
M> {
M> lock();
M> int refcnt = ++reg()[pt];
M> unlock();
M> return refcnt;
M> }
а как иначе? ведь переносить функциональность "аллокатора счётчиков" в smart_ptr<T> нельзя, так как для T1 и T2 будут разные счетчики даже если T1 и T2 "полиморфно связаны" и T1 *p1 и T2 *p2 указывают на один и тот-же объект,
кстати поэтому я использую dynamic_cast<void*>.
2.
// /*explicit*/ - можно случайно инициализировать указателем на объект, который нельзя удалять.
// огромная ошибкоопасность.
M> /*explicit*/ smartptr(const T *pt = 0)
M> : m_pt( 0 ) //init member to zero!
M> {
M> reset( pt );
M> }
а в чем разница:
smartptr<X> ptr = SomeThing;
или
smartptr<X> ptr( SomeThing );
в обоих случаях это возможно.
(правда explicit закомментировал для удобства использования).
3.
// dynamic_cast - невозможно работать с типами, не являющимися полиморфными классами.
использую dynamic_cast<void*> для получения адреса объекта, потом адрес использую как ключ в std::map;
можно как-то иначе?
Здравствуйте, makes, Вы писали:
M>Вот самодельный thread safe smart pointer M>для polymorphic классов без встроенного счетчика ссылок. M>Просьба указать на потенциальные ошибки, возможности оптимизации и т.д., в общем оценить и покритиковать.
В такой ситуации необходимо вначале инкрементировать счётчик для нового объекта, а потом декрементировать для старого. В противном случае ты можешь налететь на ситуацию, что новый объект уже разрушен к моменту инкремента. Представь, есть объект А, который содержит умный указатель на объект Б. Объект А держится только одним умным указателем. Теперь мы для этого умного указателя (который держит А) делаем reset() на Б. После decrease_refcount() для А, А и Б удаляются. И потом мы пытаемся сделать increase_refcount() для уже удаленного Б.
smartptr::reset() тоже должна учитывать, что increase_refcount() может кинуть исключение. Но вроде выполнение increase_refcount() первым действием решает проблему.
Что касается производительности и оптимизации, то прямо скажем, что текущий дизайн достаточно плох. Тут и теоретическая сложность операций O(logN), т.е. чем больше объектов, тем тормознее система. Тут и плохая реализация — операции требуют аллокации вспомогательных объектов + отвратительная локальность обращений. Тут и полная централизация, которая глушит любую concurrency на корню и вносит колоссальные издержки при исполнении на физически параллельном железе.
Самая эффективная (из простых) реализация будет использовать интрузивный счётчик ссылок. Пользователь будет обязан либо наследовать свои классы от специального вспомогательного объекта, либо включать специальный объект членом в свои классы, либо ты предоставляешь пользователю свою функцию аллокации памяти, которая размещает перед объектом данные, необходимые для подсчёта ссылок.
Как некоторые подпорки для текущей реализации можно предложить партицировать все данные, связанные с подсчётом ссылок, по хэшу от указателя. Что-то типа такого:
Ну и плюс использовать свой slab аллокатор для map с кэшем в каждом потоке.
Это несколько снизит теоретическую сложность до O(log(N/part_count)), несколько улучшит реализацию, и даст некоторую concurrency.
M>// порочная идея - хранить счётчики в общей помойке.
M>// во-первых, каждая операция - O(logN)
M>// во-вторых, бутылочное горлышко (невозможность двум независимым потокам параллельно работать)
M>> static int increase_refcount(const void *pt)
M>> {
M>> lock();
M>> int refcnt = ++reg()[pt];
M>> unlock();
M>> return refcnt;
M>> }
M>
M>а как иначе? ведь переносить функциональность "аллокатора счётчиков" в smart_ptr<T> нельзя, так как для T1 и T2 будут разные счетчики даже если T1 и T2 "полиморфно связаны" и T1 *p1 и T2 *p2 указывают на один и тот-же объект, M>кстати поэтому я использую dynamic_cast<void*>.
Приводи любой тип, переданный пользователем, к своему rc_base_t, и дальше с ним работай.
В любом MDT (most derived type) объекте должен быть только один rc_base_t, и он должен быть виден и доступен из всех базовых подобъектов, для которых создаются умные указатели. Обеспечение этого — обязанность пользователя.
M>2. M>
M>// /*explicit*/ - можно случайно инициализировать указателем на объект, который нельзя удалять.
M>// огромная ошибкоопасность.
M>> /*explicit*/ smartptr(const T *pt = 0)
M>> : m_pt( 0 ) //init member to zero!
M>> {
M>> reset( pt );
M>> }
M>
M>а в чем разница: M>
M>smartptr<X> ptr = SomeThing;
M>
M>или M>
M>smartptr<X> ptr( SomeThing );
M>
M>в обоих случаях это возможно. M>(правда explicit закомментировал для удобства использования).
Я думаю, что Кодт имел в виду что-то типа такого:
void foo(X& x, ...)
{
...
bar(&x);
...
}
Без explicit конструктора тут можно получить неприятные сюрпризы. Причём даже вначале этот код может работать, а потом кто-то по наивности изменяет сигнатуру функции bar(), т.к. что она начинает принимать умный указатель, к его радости весь проект компилируется успешно, однако потом начинаются неприятные сюрпризы.
Если конструктор explicit, то код не скомпилируется. Да и вообще такая запись более отражает суть происходящего:
M>все в порядке, по моему, все указатели (все типы) приводятся к void* динамик_кастом
Тут проблема даже не во множественном наследовании, а в том, что increase_refcount() должен стоять перед decrease_refcount(). Тогда == проверку можно убрать совсем.
Между прочим, в узком смысле. Одновременный reset и get из разных нитей недопустим.
Было бы интересно на полностью потокобезопасные умные указатели на атоомарных операциях.
Русский военный корабль идёт ко дну!
Re[5]: между прочим boost::shared_ptr thread-safe'ный
Здравствуйте, remark, Вы писали:
R>По-моему, хотя бы раз такой велосипед стоит сделать самому. Для само-развития. По-крайней мере я не жалею, что делал...
В учебных целях и в самом деле полезно, но на реальном проекте боже упаси...
Твоя основная идея — в том, чтобы
— сделать подсчёт ссылок неинтрузивным — т.е. вынести счётчик за пределы объекта
— при этом сделать его прозрачным
К сожалению, в С++ слишком просто и легко получить голый указатель на что попало, в том числе, на объекты, которые нельзя подсчитать-и-пристрелить.
У прозрачности оказывается слишком высокая цена: бутылочное горлышко, проблемы с не полиморфными классами, ошибкоопасность.
Вот и подумай: нужна ли она тебе?
Если нужна для узкого семейства классов — тогда подумай, нужна ли тебе тогда неинтрузивность?
Здравствуйте, Кодт, Вы писали:
К>// гонки при инициализации статической переменной M>> static register_type reg; M>> return reg; M>> }
/* ... */ К>// гонки при инициализации, возможность получить несколько экземпляров M>> static CRITICAL_SECTION *pcs = 0; M>> if ( !pcs ) M>> {
а как избавиться от гонок? инициализация библиотеки в main/WinMain?
К>// порочная идея — хранить счётчики в общей помойке. К>// во-первых, каждая операция — O(logN) К>// во-вторых, бутылочное горлышко (невозможность двум независимым потокам параллельно работать)
M>> static int increase_refcount(const void *pt) M>> { M>> lock();
M>> int refcnt = ++reg()[pt];
M>> unlock(); M>> return refcnt; M>> }
M>> static int decrease_refcount(const void *pt) M>> { M>> lock(); M>> int refcnt = --reg()[pt];
M>> if ( !refcnt ) M>> reg().erase(pt);
M>> unlock(); M>> return refcnt; M>> } M>>};
"... O(logN) ..." — это, я так понимаю, неизбежно при "неинтрузивности"?
"... невозможность двум независимым потокам параллельно работать ..." — а в чем проблема, один поток меняет reg, другой ждет?
я правильно понял: лучше использовать интрузивные счетчики?
А можно ссылку научно-популярную, про агрегаты почитать?
К>// при наличии operator T* определение operator bool избыточно К>// кстати, здравствуй, хинди-код; можно было короче: return m_pt!=0
M>> operator bool () const { return m_pt ? true : false; }
К>// при наличии operator bool определение operator! избыточно. К>// оно потребовалось из-за неоднозначности, создавшейся из-за объявления operator bool вдобавок к operator T*
то есть при наличии operator T* избыточны оба оператора: bool и !?
"... хинди-код; можно было короче: return m_pt!=0 ..." -- "иудаизм" — это от ОЧЕНЬ большого ума.
Здравствуйте, makes, Вы писали:
M>а как избавиться от гонок? инициализация библиотеки в main/WinMain?
Вообще, корректные синглетоны — это очень увлекательное дело.
Можно инициализировать явно — но для такой низкоуровневой библиотеки это выглядит издевательски.
Если хочешь разобраться, читай Александреску и Саттера, они про синглетоны много написали.
Самое примитивное решение — это завести глобальные переменные CCriticalSection и map<void*,int>.
Их конструкторы выполнятся до main в однопоточное время, и не создадут проблем.
Там будет другая беда: нужно как-то договориться с линкером. То есть, либо поместить их в отдельный .cpp, либо специальными трюками объявить их selectany.
У MSVC это делается __declspec(selectany), GCC — не знаю, есть ли такой атрибут.
Без расширений языка — используется тот факт, что inline-функции и статические члены шаблонов являются selectany. Поэтому и говорю: "трюки".
К>>// порочная идея — хранить счётчики в общей помойке. К>>// во-первых, каждая операция — O(logN) К>>// во-вторых, бутылочное горлышко (невозможность двум независимым потокам параллельно работать)
M>"... O(logN) ..." — это, я так понимаю, неизбежно при "неинтрузивности"?
Это следствие того, что у тебя монолитный реестр подконтрольных объектов, сделанный на деревянном контейнере.
Хэш-таблица, кстати, тоже не выручит. Хотя надо смотреть, сколько объектов живёт в системе одновременно.
M>"... невозможность двум независимым потокам параллельно работать ..." — а в чем проблема, один поток меняет reg, другой ждет?
Проблема в том, что при каждом обращении к умным указателям (и, следовательно, к реестру) все потоки ломятся в единственную дверь строго по очереди.
Ладно бы, если два потока пытались работать с указателями на один объект (и, следовательно, с одним счётчиком). Тут сериализация необходима. А когда с разными объектами — зачем?
M>я правильно понял: лучше использовать интрузивные счетчики?
Достаточно, чтобы каждый указатель имел прямой доступ к своему счётчику, а не лазил туда через реестр.
Тогда тормоза будут лишь в точках создания-убивания объектов.
То есть,
M>А можно ссылку научно-популярную, про агрегаты почитать?
Я называю там агрегатами то, что входит в состав объекта:
— базовые подобъекты
— члены
— сторонние объекты, которыми данный объект монопольно владеет и распоряжается
Очевидно, что указатель сам по себе не распоряжается указуемым объектом, а только служит для управления его видимостью и временем жизни.
А вот фасад PIMPL, сделанный из указателя — распоряжается своим единственным агрегатом.
M>то есть при наличии operator T* избыточны оба оператора: bool и !?
Именно.
Дистиллированный пример
struct foo
{
// просто некое определение приведения к указателю. без смысла.operator void const* () const { return this; }
};
int main()
{
foo f;
// по-всякому приводим объект к boolbool b1 = f;
bool b2 = !f;
bool b3 = f ? true : false;
bool b4 = f && f;
}
Иногда, чтобы избежать нежелательных приведений к bool (b1), оставив лишь логику (b2..b4), вводят специально приведение к unspecified bool type.
Посмотри в бусте, как это делается, или поищи на RSDN.
Увидел ещё одну беду и огорчение. А именно: smartptr в reset делает delete m_pt.
Теперь допустим, что этот m_pt — база без виртуального деструктора. UB.
Причём компилироваться всё будет: для dynamic_cast<void*> необходимо и достаточно наличие хоть какой-нибудь виртуальной функции, а не обязательно деструктора.
Вот, в том числе против этой проблемы борются в boost::shared_ptr, сохраняя функцию-разрушитель исходного объекта.