Я не знал, как назвать это, может быть, «follow_ptr», «self_updating_ptr» или «stalking_ptr» или что-то в этом роде. Пока это называется Идентификатор.
Я пытаюсь достичь обертки указателя, которая всегда будет ссылаться на один и тот же объект, даже когда этот объект перемещается в памяти (довольно частым примером является изменение размеров вектора, а также такие алгоритмы, как std :: remove_if, которые могут перемещать элементы).
EDIT: одно из требований — разрешить хранение объектов в последовательных контейнерах (например, vector и deque) без потери последовательного хранилища, как при использовании unique_ptr или shared_ptr. Вся эта система не предназначена для заботы о собственности.
Это плохо для использования термина «умный указатель в исходном заголовке», это умно в том смысле, что он следует за заостренным объектом, а не за указателем наблюдателя, который этого не сделает.
Требование состоит в том, чтобы объект хранился в «идентифицированном» классе. Этот класс необходим для обновления всех идентификаторов.
Уловка заключается в двойном косвенном обращении, когда необработанный указатель, живущий в куче, будет указывать на объект, который нужно преследовать:
#include <memory>
#include <stdexcept>
template <typename T>
class Identifier;
template <typename T>
class Identified;
// A pointer to an identified object. This object lives in the heap and is used to share information with all identifiers about the object moving in memory.
template <typename T>
class Inner_identifier
{
public:
Inner_identifier() = default;
Inner_identifier(T* identified) noexcept : identified{identified} {}
Inner_identifier(const Inner_identifier& copy) = delete;
Inner_identifier& operator=(const Inner_identifier& copy) = delete;
Inner_identifier(Inner_identifier&& move) = delete;
Inner_identifier& operator=(Inner_identifier&& move) = delete;
T* identified{nullptr};
};
Идентификатор, или сталкер, действует как промежуточный между интеллектуальным указателем и необязательным. Идея состоит в том, что если идентификаторы переживают объект, они все еще действительны (при условии, что пользователь проверяет с помощью has_value перед их использованием, например, с необязательным).
Я не уверен, следует ли мне просто удалить конструктор по умолчанию, чтобы он всегда был уверен, что указатель идентификатора на Inner_identifier всегда действителен, и я могу избавиться от некоторых проверок. А пока я оставил это, чтобы упростить написание примера.
template <typename T>
class Identifier
{
public:
Identifier() = default;
Identifier(Identified<T>& identified) : inner_identifier{identified.inner_identifier} {}
Identifier& operator=(Identified<T>& identified) { inner_identifier = identified.inner_identifier; return *this; }
Identifier(const Identifier& copy) = default;
Identifier& operator=(const Identifier& copy) = default;
Identifier(Identifier&& move) = default;
Identifier& operator=(Identifier&& move) = default;
const T& operator* () const { check_all(); return *inner_identifier->identified; }
T& operator* () { check_all(); return *inner_identifier->identified; }
const T* operator->() const { check_all(); return inner_identifier->identified; }
T* operator->() { check_all(); return inner_identifier->identified; }
const T* get() const { check_initialized(); return inner_identifier->identified; }
T* get() { check_initialized(); return inner_identifier->identified; }
bool has_value() const noexcept { return inner_identifier && inner_identifier->identified != nullptr; }
explicit operator bool() const noexcept { return has_value(); }
private:
std::shared_ptr<Inner_identifier<T>> inner_identifier{nullptr};
void check_initialized() const
{
#ifndef NDEBUG
if (!inner_identifier) { throw std::runtime_error{"Trying to use an uninitialized Identifier."}; }
#endif
}
void check_has_value() const
{
#ifndef NDEBUG
if (inner_identifier->identified == nullptr) { throw std::runtime_error{"Trying to retrive object from an identifier which identified object had already been destroyed."}; }
#endif
}
void check_all() const { check_initialized(); check_has_value(); }
};
Наконец, класс Identified, содержащий экземпляр объекта, на который указывает один или несколько идентификаторов. Он отвечает за обновление Inner_identifier всякий раз, когда он перемещается в памяти с помощью конструктора перемещения или присваивания перемещения. Напротив, конструктор копии следит за тем, чтобы новая копия имела свой собственный новый Inner_identifier, и все существующие идентификаторы по-прежнему работают с копируемым экземпляром. После уничтожения Inner_identifier обнуляется, но он будет существовать для ссылки до тех пор, пока существует хотя бы один идентификатор для ныне несуществующего объекта (следовательно, внутренние shared_ptrs)
template <typename T>
class Identified
{
friend class Identifier<T>;
public:
template <typename ...Args>
Identified(Args&&... args) : object{std::forward<Args>(args)...}, inner_identifier{std::make_shared<Inner_identifier<T>>(&object)} {}
Identified(Identified& copy) : Identified{static_cast<const Identified&>(copy)} {}
Identified(const Identified& copy) : object{copy.object}, inner_identifier{std::make_shared<Inner_identifier<T>>(&object)} {}
Identified& operator=(const Identified& copy) { object = copy.object; return *this; } //Note: no need to reassign the pointer, already points to current instance
Identified(Identified&& move) noexcept : object{std::move(move.object)}, inner_identifier{std::move(move.inner_identifier)} { inner_identifier->identified = &object; }
Identified& operator=(Identified&& move) noexcept { object = std::move(move.object); inner_identifier = std::move(move.inner_identifier); inner_identifier->identified = &object; return *this; }
~Identified() { if (inner_identifier) { inner_identifier->identified = nullptr; } }
const T& operator* () const { return *get(); }
T& operator* () { return *get(); }
const T* operator->() const { return get(); }
T* operator->() { return get(); }
const T* get() const
{
#ifndef NDEBUG
if (!inner_identifier || inner_identifier->identified == nullptr) { throw std::runtime_error{"Attempting to retrive object from an identifier which identified object had already been destroyed."}; }
#endif
return &object;
}
T* get()
{
#ifndef NDEBUG
if (!inner_identifier || inner_identifier->identified == nullptr) { throw std::runtime_error{"Attempting to retrive object from an identifier which identified object had already been destroyed."}; }
#endif
return &object;
}
T object;
private:
std::shared_ptr<Inner_identifier<T>> inner_identifier;
};
Помимо критики, я хотел бы получить совет по именованию. Если бы я назвал идентификатор «follow_ptr», «self_updating_ptr» или «stalking_ptr», я не знаю, как вызвать два других класса.
Если не считать первой заглавной буквы классов, чувствуется ли интерфейс достаточно «стандартным»?
Вот пример использования при компиляции в режиме отладки для исключений:
#include <stdexcept>
#include <iostream>
#include <vector>
#include <algorithm>
struct Base
{
int tmp; bool enabled = true; bool alive = true;
Base(int tmp) : tmp(tmp) {}
virtual volatile void f() { std::cout << "Base::f" << tmp << std::endl; };
void g() { std::cout << "Base::g" << tmp << std::endl; };
};
struct TmpA : public Base
{
TmpA(int tmp) : Base(tmp) {}
virtual volatile void f() override { std::cout << "TmpA::f" << tmp << std::endl; };
void g() { std::cout << "TmpA::g" << tmp << std::endl;/**/ };
};
int main()
{
//Create empty identifiers
Identifier<TmpA> idn;
Identifier<TmpA> id1;
Identifier<TmpA> id5;
std::vector<Identified<TmpA>> vec;
if (true)
{
//Create some data and assign iit to identifiers
Identified<TmpA> identified_a1{1};
Identified<TmpA> identified_will_die{0};
idn = identified_will_die;
id1 = identified_a1;
id5 = vec.emplace_back(5);
//Move some identified objects around, this also causes the vector to grow, moving the object Identified by id5.
vec.emplace_back(std::move(identified_a1));
}
std::cout << " _______________________________________________ " << std::endl;
std::cout << "vec[0]: " << " "; try { vec[0]->f(); } catch (std::exception& e) { std::cout << e.what() << std::endl; }
std::cout << "vec[1]: " << " "; try { vec[1]->f(); } catch (std::exception& e) { std::cout << e.what() << std::endl; }
std::cout << "id1: " << " "; try { id1->f(); } catch (std::exception& e) { std::cout << e.what() << std::endl; }
std::cout << "id5: " << " "; try { id5->f(); } catch (std::exception& e) { std::cout << e.what() << std::endl; }
std::cout << "null: " << " "; try { idn->f(); } catch (std::exception& e) { std::cout << e.what() << std::endl; }
//Move some identified objects around
std::partition(vec.begin(), vec.end(), [](Identified<TmpA>& idobj) { return idobj->tmp > 2; });
std::cout << " _______________________________________________ " << std::endl;
std::cout << "vec[0]: " << " "; try { vec[0]->f(); } catch (std::exception& e) { std::cout << e.what() << std::endl; }
std::cout << "vec[1]: " << " "; try { vec[1]->f(); } catch (std::exception& e) { std::cout << e.what() << std::endl; }
std::cout << "id1: " << " "; try { id1->f(); } catch (std::exception& e) { std::cout << e.what() << std::endl; }
std::cout << "id5: " << " "; try { id5->f(); } catch (std::exception& e) { std::cout << e.what() << std::endl; }
std::cout << "null: " << " "; try { idn->f(); } catch (std::exception& e) { std::cout << e.what() << std::endl; }
}
2 ответа
Сохранение объектов как можно более последовательными
После прочтения комментариев кажется, что наиболее важным вариантом использования является отслеживание перемещений объектов в контейнерах, но мы хотим, чтобы эти объекты были последовательными и были максимально удобными для кеширования. Также вероятно, что вы хотите отслеживать не все объекты в контейнере, а лишь некоторые из них. В этом случае ваша реализация имеет некоторые недостатки. Главный из них — вы храните std::unique_ptr
вместе с каждым объектом, поэтому они больше не являются последовательными. Считайте, что у вас был:
std::vector<T> vec;
Тогда в памяти у вас есть:
T0 T1 T2 T3 ...
Но теперь вы хотите отследить некоторые из T
s, то вы должны написать:
std::vector<Identified<T>> vec;
Тогда в памяти у вас будет:
T0 std::shared_ptr<Inner_identifier<T>> T1 std::shared_ptr<...> T2 ...
Можем ли мы сделать лучше? В идеале мы хотим получить T
упакованы вплотную, как в исходном векторе. Мы можем получить это, если переместим отслеживание в отдельный глобальный реестр:
template<typename T>
std::unordered_map<T *, std::shared_ptr<T *>> registry;
Теперь, когда вы создаете экземпляр Identified<T>
, вы хотите, чтобы он поместил адрес созданного объекта в эту карту. Когда объект перемещается, вы должны соответственно обновить карту и внутренний идентификатор. Однако, если никто не отслеживает данный объект, его даже не нужно хранить в реестре, поэтому мы можем отложить добавление объекта в реестр до тех пор, пока кто-то не захочет создать Identifier<T>
от него.
Вот пример того, как это могло бы выглядеть:
template<typename T>
class Identifier {
std::shared_ptr<T *> object;
public:
Identifier(Identified<T> &identified) {
// Check if this object is already in the registry
if (auto it = registry<T>.find(&identified.object); it != registry<T>.end()) {
// Yes, we also want a reference to it
object = *it;
} else {
// No, make a new entry in the registry
object.reset(&identified.object);
registry<T>[&object] = object;
}
}
T &operator*() {
if (!*object)
throw ...;
return **object;
}
...
};
template<typename T>
class Identified {
T object;
public:
// Constructor just constructs the object
template <typename ...Args>
Identified(Args&&... args): object{std::forward<Args>(args)...} {}
// Move constructor has to update the registry
Identified(Identified &&other) {
// Check if this object is already in the registry
if (auto it = registry<T>.find(&other.object); it != registry<T>.end()) {
// Yes, update the value
*it.reset(&object);
// And also update the key
auto nh = registry<T>.extract(it);
nh.key() = &object;
registry<T>.insert(std::move(nh));
}
// Now move the actual contents of the object
object = std::move(other.object);
}
...
};
Именование вещей
Попробуйте переименовать классы, чтобы лучше передать их назначение:
Identified
->Trackable
Identifier
->Tracker
Я не думаю, что нужен Inner_identifier
если у вас есть реестр.
Основная проблема заключается в том, что этот механизм не является потокобезопасным. Если вы удерживаете идентификатор в одном потоке и перемещаете его в другой поток, это приводит к скачку данных, поскольку вы не используете какие-либо процедуры синхронизации памяти.
Более того, вы не можете переместить объект, когда его использует другой поток. И в настоящее время нет возможности даже проверить его, используется ли он или что-то в этом роде. Чтобы исправить это, вам нужно блокировать мьютекс каждый раз, когда вы его используете или перемещаете. Это означает, что перемещение может занять много времени из-за длительного ожидания, и если у вас их много, вам придется заблокировать такое количество мьютексов — что является хорошим источником взаимоблокировок, если вы не применяете специальные алгоритмы, которые гарантируют, что мьютексы будет заблокирован безопасным способом.
Более того, любое удобство использования кеша, которого вы надеялись достичь с помощью этого объекта, будет разрушено, как только сработает какое-либо ограждение памяти (только расслабленный операция с памятью может не испортить его, но вам понадобится приобретать и выпускать которые требуют повторного кэширования в общую память).
ммм, не могли бы вы подробнее рассказать, как операция перемещения должна взаимодействовать с этим реестром? Если T * — это ключ, разве вы не потеряете ключ при перемещении объекта?
— Барнак
Я добавил пример. Хитрость заключается в том, чтобы обновлять и ключ, и значение на карте при перемещении объекта.
— Г. Сон