Pull to refresh
17
0
Send message
А вот тут не совсем понятно, почему событие должно обрабатывать исключения в клиентском коде (если можно так назвать обработчики).


Обрабатывать чужие исключения конечно не нужно. Вопрос, в каком состоянии будет евент, если произойдет исключение в обработчике. Про гаратнии можно почитать здесь Exception_safety. Стандартные контейнеры вроде в большинстве случаев имеют Strong exception guarantee. Для класса общего пользования иметь Basic exception guarantee вполне неплохо.

        void operator()( TParams... params )
        {
            TMyHandlerRunner newHandlerRunner( m_core );

            m_core.coreMutex.lock_shared();
            auto it = m_handlerRunners.insert( m_handlerRunners.end(), &newHandlerRunner );
            m_core.coreMutex.unlock_shared();
             //
            // если исключение бросится здесь
            //
            newHandlerRunner.run( params... );
            //
            // то вот это все не отработает и в m_handlerRunners застрянет указатель на локальный newHandlerRunner 
            //
            m_core.coreMutex.lock_shared();
            m_handlerRunners.erase( it );
            m_core.coreMutex.unlock_shared();
        }

Ну и удалять обработчик, если он сейчас работает нельзя. Если это функтор и в середине его работы происходит отписка и он дальше выполняет какой-то код, то мы в момент отписки удалим функтор и весь остальной код фунтора будет работать на удаленном объекте. Здесь надо помечать обрабртчик на удаление, а удалять в функции рассылки (при условии что он не рассылается в паралельном цикле рассылки)

Вообще в реализациях евентов есть подводные камни, которые в стандартных реализациях решены или документированы (в большинстве своем).

Хотя мы используем самописный евент, который за годы эксплуатации не раз подправляли.
Мне, в свое время, понравилась статья Потокобезопасные сигналы, которыми действительно удобно пользоваться

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

Честно говоря не вижу как решена проблема с параллельным выполнением «перебора». Или вызовом «перебора» из какого-то обработчика. Похоже что такие ситуации никак не накрыты.

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

       void operator()( TParams... params )
        {
            m_handlerListMutex.lock_shared();
            
            m_isCurrentItRemoved = false;
            m_currentIt = m_handlers.begin();
            while( m_currentIt != m_handlers.end() )
            {
                m_handlerListMutex.unlock_shared();
                // !!!! в следующей строке может случиться все что угодно !!!!
                ( *m_currentIt )->call( params... );
                m_handlerListMutex.lock_shared();

                if( m_isCurrentItRemoved )
                {
                    m_isCurrentItRemoved = false;

                    TEventHandlerIt removedIt = m_currentIt;
                    ++m_currentIt;

                    deleteHandler( removedIt );
                }
                else
                {
                    ++m_currentIt;
                }
            }

            m_handlerListMutex.unlock_shared();
        }
На здоровье.

Кроме assign/front, еще есть несколько функций приводящих к таким же проблемам.
Да и контейнер такой не только вектор.
А, это да, это поможет. Но только это требует дополнительных проверок в рантайме, а один из принципов C++, которого стараются придерживаться — не платить за то, что вы не используете.

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

А это тоже интересный вопрос.

Вопрос конечно спорный, но там явно выражена причина убирания проверки
The reason: let’s make moves as fast as possible and take advantage of the fact that Self-Moves Shouldn’t Happen.

Сделать перемещение как можно быстрым — вполне законное желание, т.к. в большинстве случаев перемещение не делает новых аллокаций. С копированием сложнее, она гораздо «тяжелее» перемещения и на его фоне доп проверка опять же — ничто.
Благо это проверяется тривиально.

Я имел ввиду проверку, что адрес параметра в assign не лежит между [begin, end].
Это чем-то напоминает в операторе присваивания проверку, что параметр не равен this, та же логика — спецобработка если модифицируем себя с помощью себя же.
Вроде std::memmove дает какие-то гарантии при оверлапе ксков данных.
Тут тоже можно потребовать валидность таких конструкций, когда происходит модификация вектора и используется его элемент. Благо это проверяется тривиально.
Недавно обнаружил у себя долгоживущий баг:
std::vector<int> a = { 3 };
a.assign(10, a.front());

возможно стоит сделать такие вещи валидными по стандарту?
Недавно столкнулся со старой ошибкой в своем коде. Решил проверить, как PVS-Studio отработает мою ошибку. К сожалению он не нашел не только мою ошибку, но и более простую.

Простой вариант:
std::vector<int> a;
int b = a.front();

(причем вариант «int b = a[0];» детектируется).

Более сложный вариант:
std::vector<int> a = { 3 };
a.assign(10, a.front());
Валидный останется объект или не валидный — не очень важно.
Главное, семантика перемещения. Объект а после перемещения хранит непонятно что (если move реализован через swap, то в а лежит содержимое b), и вроде нет никаких причин работать с а, кроме как присвоить ему новое значение.
Мне кажется это главным изъяном операции перемещения введеной в С++11 — появление объектов с непонятным состоянием, которые по идее должны закончить свою область видимости после перемещения.
А у вас есть диагностика на использование значения, которое переместили?
Что-то вроде этого:
    std::vector<int> a = ...;
    ...
    auto b = std::move(a);
    ...
    auto s = a.size(); // ???
По сути так и есть. Мета-классы применяются перед компиляцией.
Никакого усложнения синтаксиса нет — на вход метакалссы принимают синтаксически валидный С++ класс. Сама реализация пишется вполне обычными С++ циклами, условными операторами и т.п. Единственное дополнение — знак $, который они заменят на что-то другое.

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

Подход Qt — это очень серьезный компромисс, по сути других вариантов не было в тот момент.
Они используют синтаксис макросов, что достаточно ограниченно в выразительных средствах и нам все равно приходится запоминать синтаксис разных Q_PROPERTY или Q_ENUM.
Похоже лед тронулся:
metaclasses
Возможно это просто полиси в trackable, которое указывать как интерпретировать перемещение/копирование объекта: либо должны инвалидироваться ссылки, либо нет.

Если не возражаете, добавлю такой вид указателей как ваш SafePtr к себе в проект, ну и заглушку с простым указателем с таким же интерфейсом для консистенции? Что бы можно было легко переключать какие указатели использовать в коде.
Тут речь о консистентности.

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

Не совсем понял вопрос.


То есть технически надо сделать так, что бы мы «стреляли» не только при удалении объекта, но и при перемещении/копировании в него нового объекта.
Вопрос как это реализовать?
Но из-за того, что указатели бывают shared — придётся в этом коде сделать ряд приседаний, чтобы именно в этом случае SafePtr мог спокойно пережить выход за скоуп owner'а.

Здесь непонятно, что имелось в виду. Хотя для shared_ptr вообще SafePtr не нужен, просто для полноты?

AddRef/Release воткнул для Release-версии, чтобы owner просто считал количество ссылок на него. Если при разрушении не 0 — помирать с исключением в деструкторе (привет дяде Александреску и его folly).

Похоже вы что-то недописали в коде. или я что-то не понимаю, тут ничего не будет падать:
    ~SafePtr()
    {
        auto tracker = m_tracker.lock();
        if (tracker)
            tracker->Release();
    }


Схлопнуть до того количества типов, о котором вы говорите — не выйдет.

ну это я на основании вашего «proof of concept», сколько там ковариаций, столько и у меня: о)

Возможно, что в traceable вообще type erasure втыкать придётся.

как я говорил, для shared_ptr такие указатели как SafePtr вообще бесполезны — вся функциональность и так есть из коробки. По простому можно просто запретить создавать SafePtr от shared_ptr. Ну или сделать независимую специализацию trackable и SafePtr для shared_ptr и не объединять код с применением type erasure
из-за которых отследить фактическую инвалидацию элемента в любых случаях — невозможно

странно, мне казалось все просто. какой объект разрушается, такие указатели и не валидны.
я понимаю, что семантически после удаления элемента в векторе, все последующие элементы перемещаются вправо, но по факту элементы валидны, кроме последнего. вы также ничем не гарантируете, что у объекта на стеке не вызвался оператор перемещения и теперь это «немного» другой объект.
Контейнер/не контейнер — без разницы. Гарантий нет.

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

Честно говоря никогда о таком не слышал. Мне кажется это очень субъективное мнение.

Поэтому да, в моем представлении время жизни trackable-объекта должно быть всегда меньше, чем того объекта, на который он указывает. И не важно — на стеке этот объект, в контейнере или в динамической памяти.

У вас время жизни объекта ограничивается деструктором и оператором перемещения, как вы сможете реализовать свой SafePtr в таком случае?
Обязать клиента поддерживать эти инварианты?
Не понятно.
В Rustе можно иметь две семантики, а в С++ нельзя?
любопытно…

я смотрю m_refs а с ним и AddRef/Release вообще не нужны там: о)

вы делаете неявное предположение, что SafePtr<T, detail::OwnerTag> должен жить столько же, сколько и объект, на который указывает.

По сути можно выкинуть TrackableBase и Trackable.
Переименовать SafePtr<T, detail::OwnerTag> в Trackable, и оставить там shared_ptr<Trackable, empty_deleter>, который проинициализировать this.
A SafePtr<T, detail::ReferenceTag> переименовать просто в SafePtr[T] и иметь указатель на T и weak_ptr[Trackable].

Идея ясна. Более эффективно убрать shared_ptr/weak_ptr и продублировать их функциональность с помощью std::atomic_int, что вы, наверное, и хотели сделать. Таким образом мы убираем аллоцирования в shared_ptr.

Как я уже говорил, у меня была схожая идея, но она не работает с аллокаторами. Когда один trackable следит за временем жизни нескольких объектов.

И да, не пугайтесь — это C++17. :)

   SafePtr(T* obj)
        : m_trackable(new detail::Trackable(obj))

Шо, make_shared до сих пор не завезли???: о)

Кстати, safe_ptr от electronics arts мне совсем не понравился.
Вот, кстати, фишка с контейнерами из статьи вообще не ясна. Что именно и к чему хочется прикрутить?

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

Какой сценарий?

Ну например какой-то кеш, который может инвалидороваться:
    std::map<key, cached_value_type> cache;
    cached_value_type* get_value(key)
    {
        // создать кеш значение для ключа, если его еще нет
        ...
        return cache[key];
    }

да и везде, где мы передаем элемент контейнера вовне.

Заменяем std::map на rsl::track::map и тип возвращаемого указателя с cached_value_type* на rsl::track::pointer<cached_value_type>, и вуаля — при инвалидации или удалении кеша мы ловим повисшие ссылки на него.

Information

Rating
Does not participate
Registered
Activity