Каковы плюсы и минусы использования интеллектуальных указателей в качестве «не владеющих ссылками»?

Когда объект должен ссылаться на другой объект, не «владея им» (то есть не несет ответственности за его время жизни), один из способов — просто использовать для этого необработанные указатели или необработанные ссылки, как в этом примере:

class Node
{
std::vector<Edge*> incidentEdges;
};

class Edge
{
Node* startNode;
Node* endNode;
};

class Graph
{
std::vector<std::unique_ptr<Node*>> nodes;
std::vector<std::unique_ptr<Edge*>> edges;
};

(пожалуйста, сэкономьте время, чтобы прокомментировать существование более эффективных структур данных для графиков, это моя область знаний, а не вопрос.)

Graph отвечает за время жизни узлов и ребер и отвечает за то, чтобы указатели в Node а также Edge не висят. Но если программист не сможет этого сделать, то существует риск неопределенного поведения.

Но, учитывая накладные расходы на подсчет ссылок, можно строго убедиться, что при использовании умных указателей не может произойти неопределенное поведение. Вместо этого он грациозно потерпит крах. Это гарантирует, что это произойдет в кратчайшие сроки (избегайте повреждения данных) и не останется незамеченным. Вот одна из возможных реализаций:

(редактировать: исправлена ​​реализация, более подробная информация в Yakk ответ. Огромное спасибо!)

template <class T>
using owning_ptr = std::shared_ptr<T>;

template <class T>
class nonowning_ptr
{
std::weak_ptr p_;

public:
nonowning_ptr() : p_() {}
nonowning_ptr(const nonowning_ptr & p) : p_(p.p_) {}
nonowning_ptr(const owning_ptr<T> & p) : p_(p) {}

// checked dereferencing
owning_ptr<T> get() const
{
if (auto sp = p_.lock())
{
return sp.get();
}
else
{
logUsefulInfo();
saveRecoverableUserData();
nicelyInformUserAboutError();
abort(); // or throw exception
}
}

T & operator*() const = delete; // cannot be made safe
owning_ptr<T> operator->() const { return get(); }

// [...] other methods forwarding weak_ptr functionality
};

class Node
{
std::vector<nonowning_ptr<Edge>> incidentEdges;
};

class Edge
{
nonowning_ptr<Node> startNode;
nonowning_ptr<Node> endNode;
};

class Graph
{
std::vector<owning_ptr<Node>>> nodes;
std::vector<owning_ptr<Edge>>> edges;
};

Мой вопрос: помимо очевидного соотношения производительности и безопасности, каковы плюсы и минусы каждого подхода?

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

1

Решение

Мой вопрос: помимо очевидного соотношения производительности и безопасности, каковы плюсы и минусы каждого подхода?

Игнорирование того факта, что нет других вопросов для умных указателей Кроме того производительность и безопасность (производительность Зачем мы не просто позволяем GC справиться с этим безопасно), есть тот факт, что ваш nonowning_ptr класс ужасно сломан.

Ваш get Функция возвращает голый указатель. Тем не менее, в вашем коде нет никакой гарантии, что любой пользователь get получит либо действительный указатель, либо NULL,

В тот самый момент, когда вы уничтожаете shared_ptr вернулся weak_ptr::lockВы удаляете единственное, что сохраняет эту память действительной. Это означает, что, если кто-то придет и удалит последний shared_ptr в эту память, пока у вас есть ваш T*, ты пьян.

Потоки, в частности, разрушают ваши иллюзии безопасности.

Таким образом, самый важный «жулик» nonowning_ptr это что сломано; это не безопаснее, чем T*,

6

Другие решения

Ваша конструкция имеет проблему в том, что если другой поток или путь выполнения (скажем, несколько аргументов для вызова функции) изменяет shared_ptr в основе вашего weak_ptr, проверка на срок службы будет сделана, и, прежде чем использовать его, вы получите UB.

Чтобы уменьшить это, T * get() должно быть std::shared_ptr<T> get(), А также operator-> должен также вернуться std::shared_ptr<T>, Хотя это кажется непрактичным, на самом деле это работает из-за забавного способа -> определяется в C ++ для авто-рекурсии. (a-> определяется как (*a). если a тип указателя, и (a.operator->())-> иначе. Так что ваши -> возвращает shared_ptrкоторый затем имеет -> вызывается по нему, который затем возвращает указатель. Это гарантирует время жизни указателя, который вы делаете -> на достаточно долго.)

// checked dereferencing
std::shared_ptr<T> get() const
{
if (auto sp = lock())
return sp;
fail();
}

void fail() { abort() } // or whatever
T & operator*() const = delete; // cannot be made safe
std::shared_ptr<T> operator->() const { return get(); } // works, magically

operator std::shared_ptr<T>() const { return lock(); }
std::shared_ptr<T> lock() const { return p_.lock(); }

сейчас p->foo(); есть (в силе) p->get()->foo(), Время жизни get() shared_ptr возвращаемое значение длиннее, чем вызов foo()так что все безопасно как дома.

Все еще есть дыра в T& operator() вызов, где ссылка может пережить свой собственный объект, но это по крайней мере исправляет -> отверстие.

Вы можете запретить T& operator*() полностью для безопасности.

shared_reference<T> может быть написано, чтобы исправить эту последнюю дыру, но operator. еще не доступен

Точно так же, operator shared_ptr<T>() было бы хорошо, и .lock() метод, чтобы разрешить временное многострочное владение. Может быть даже explicit operator bool() но это сталкивается с проблемой «проверьте, затем сделайте, но проверка может быть недействительной перед задачей», которая имеет общие указатели и файловые операции.

4

По вопросам рекламы [email protected]