All streams
Search
Write a publication
Pull to refresh
56
0
Сергей Садовников @FlexFerrum

Пользователь

Send message

С контейнерами все грустно. Как мне кажется, вы пытаетесь предоставить гарантий больше, чем у вас вообще есть. То, что у вас trackable привязан к аллокатору, а не к аллоцированному объекту делает его фактически бесполезным. map и list аллоцируют память под каждый элемент. Вектор — пачками. Дека — чанками. При этом все они имеют массу оптимизаций, из-за которых отследить фактическую инвалидацию элемента в любых случаях — невозможно. Вы вот в тестах на vector проверяете, что при erase хвостовые указатели инвалидируются, а не то, что объект, на который указывает удалённый указатель, становится невалидным. Согласитесь, это разные вещи. Код, в котором есть указатель на удалённый объект, может быть очень "удивлён", что значение объекта внезапно сменилось. Это допустимо с голыми указателями, но такого не ожидаешь от смарт-указателей которые, вроде как, от этого гарантируют. Поэтому да, в моем представлении время жизни trackable-объекта должно быть всегда меньше, чем того объекта, на который он указывает. И не важно — на стеке этот объект, в контейнере или в динамической памяти.

Отвечаю без цитат, ибо с мобилы.


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


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


Схлопнуть до того количества типов, о котором вы говорите — не выйдет. Причины три: ковариантность простого указателя и указателя на константу, ковариантность указателя на базовый и на производные типы, а также специализация для shared_ptr. То есть в случае, если owner конструируется от shared_ptr, то reference-версия должна стать этаким синонимом weak_ptr для этого shared'а. В остальных случаях — указателем на потроха owner'а. Возможно, что в traceable вообще type erasure втыкать придётся.
Про работу с контейнерами — напишу отдельно. Там всё грустно, на мой взгляд. А make_shared в стандарт завезли, куда ж без него. :) Я имел в виду автоматической вывод типа при конструировании.

В общем, мой вариант SafePtr'а «в лоб» (на базе std::shared_ptr/std::weak_ptr) — здесь. Аккуратно стреляет при обращении к протухшему указателю. Без каких либо оптимизаций и специализаций под std::shared_ptr/std::unique_ptr. Чисто proof of concept.

И да, не пугайтесь — это C++17. :)
смотрю на safe_ptr.
да, прям мои мысли, когда я начинал писать свой веловипед. Потом захотелось прикрутить стандартные контейнеры и пришлось всё усложнить. Спасибо за ссылку.

Но это не тот safe_ptr, который я имел в виду. Это какой-то интрузивный смарт-поинтер со списком владения. Свой я сейчас набросаю.

гораздо интересенее момент разрушения объекта, на который есть ссылки.

По моему опыту отладки программ с большим количеством объектов с интрузивным счётчиком ссылок скажу так: гораздо важнее знать, то, какой объект «зажал» лишнюю ссылку, чем то, на что именно ссылка подвисла. Особенно если речь об объектах с временем жизни сопоставимым со временем жизни всей программы.

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

Вот, кстати, фишка с контейнерами из статьи вообще не ясна. Что именно и к чему хочется прикрутить? Какой сценарий?
А, да. owner определяется как:
templateusing owner = T;

То есть это просто синоним типа, но «видимый» на этапе статического анализа. То есть можно написать owner<unique_ptr> и спокойно пользоваться.
owner и not_null хороши, но мне кажется not_null это полиси к любому другому указателю, а не отдельный тип.

not_null можно инициализировать любым указателем (и обычным, и smart).

очень интересная задачка. есть мысли как можно реализовать эти гарантии? запретить копирование/перемещение?

Ну, по зрелому размышлению это трансформировалось немного в другое — в safe_ptr, который «стреляет» при доступе к разрушенному указателю или при попытке разрушить объект, на который ещё висят указатели. Идея (как я сейчас посмотрел по коду) очень близка к вашему trackable, только без разделения на отдельный trackable-класс и pointer. То есть если объект конструируется из указателя (одного из вариантов) — создаётся специфичный для указателя trackable. Если конструктором копий — то pointer. Всё это за одним фасадом. Список экземпляров, «вдалеющих» trackable, по большому счёту, имеет смысл только для debug-режима, чтобы иметь возможность отследить, где был совершён доступ к разрушенному указателю. На самом деле, для этого даже простой «скрытой» связки shared_ptr + weak_ptr хватит (ловить то надо факт лочки мёртвого shared_ptr). А для релиза достаточно счётчика ссылок и проверки, что при разрушении trackable счётчик равен нулю. Оповещать указатели о том, что объект сдох — бессмысленно. Это априори ошибочная ситуация, которая в «идеальном случае» должна закончиться AV.
Как я писал в статье, для отлова ошибок, гораздо интереснее момент, когда разрушается объект, на который есть ссылки, а не когда пытаются обратится по невалидному указателю. В этом случае shared_ptr/weak_ptr никак не помогут.

На самом деле, интересно и то, и другое. Факт разрушения объекта, на который есть указатели ничего не говорит о том, где именно они «зависли».
Хм… Ваша статья написана несколько сумбурно (особенно вступительная часть), поэтому суть проблемы и решение понял, только прочитав статью дважды. И то не факт, что понял до конца. Теперь по сути. Как бы это странно не звучало, но лучший способ борьбы с dangling pointers — это не использовать «голые» указатели в коде вообще. Как, собственно, и рекомендуется в Core Guidelines. И да, действительно, в данном случае статический анализ + некоторая поддержка в рантайме может помочь больше, чем предлагаемые trackable-поинтеры. В том же GSL сейчас есть шаблонный класс owner, который просто показывает (статическому анализатору!) кто владеет объектом. Есть класс not_null, который гарантирует контракт, что хранимый в нём указатель гарантированно не будет nullptr. По такому же принципу можно добавить класс scoped_pointer, который будет гарантировать, что любая его копия умрёт раньше, чем указатель, из которого она создана и которым «временно владеет». Нарисовать такой класс, поддерживающий разные ситуации (но, очевидно, не все — инвалидирующиеся итераторы всё равно будут портить жизнь) — довольно интересная задачка.
Я к тому, что имеет смысл об этом упомянуть.
А в онлайн-версии время на каждый следующий ход тоже уменьшается, как и в оффлайн? В статье вы этот момент описали, но это создаёт дополнительный драйв в процессе.

А финал — да, знакомый. :)
То же самое с ключевым словом template в аналогичных ситуациях.
Думаю, да. Сейчас занимаюсь подготовкой доклада, где аккурат этот вопрос будет освещаться. По результатам исследования — отпишусь.
clang C++ API (в отличие от C-шного libclang) не обременено обратной совместимостью. Поэтому от версии к версии может изменяться. Иногда — существенно.
Тут же в другом фишка. Иногда (и не так редко, как кажется) требуется, чтобы область действия lock'а совпадала с областью действия оператора if. Находилась в рамках его скоупа. Любой «двухстрочный» эквивалент не соответствует этой хотелке. Вот другой пример на это же:
if (ResultCode rc = DoSomething(); rc.Failed())
{
// обрабатываем ошибку, отваливаем
}


В рамках текущего стандарта это выглядит так:
ResultCode rc;
if ((rc = DoSomething()).Failed())
{
// Обрабатываем ошибку
}

Довольно, как это говорят, ugly. Предвосхищая вопрос. Такие конструкции могут использоваться в библиотечных «обёртках» над стандартной обработкой ошибок, логированием и т. п. То есть типа:
CHECK_SUCCEEDED_RETURN(DoSomething()) << «Приключилась адская хрень»;
Сорри, второй недостаток снимается. :) Увидел в тексте, что задавать значения для элементов таки можно.
У предложенного подхода (на такого рода макросах) есть как минимум пара недостатков:
1. При большом размере перечисления компилятор начинает тупить на раскрытии всей этой макросни. А если компилятор и прожевывает, то начинает тупить IDE, которая тоже хочет всё это раскрыть в памяти.
2. Очевидно, поддерживаются перечисления, начинающиеся с 0 и нет возможности задания конкретного значения для элемента перечисления.
В своих собственных экспериментах я остановился в итоге на варианте, используемом также в llvm/clang'е, когда все такого рода enum'ы выносятся в отдельный .h-файл, поэлементно оборачиваются в макросы, а #define'ы перед включением этого .h-ника определяют то, что будет на выходе — определение enum'а, или сериализатор в строки.
На ACCU 2014 была хорошая презентация на эту тему:
Everything You Ever Wanted To Know About Move Semantics. В частности, слайды с 19 по 30.
Если нужна полная поддержка C++11 — то с этим не к студии, увы. :(

Information

Rating
Does not participate
Location
Москва и Московская обл., Россия
Date of birth
Registered
Activity