«Указатель наблюдателя» означает, что он будет обновляться, когда заостренный объект перемещается в памяти.

Я не знал, как назвать это, может быть, «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 ответа
2

Сохранение объектов как можно более последовательными

После прочтения комментариев кажется, что наиболее важным вариантом использования является отслеживание перемещений объектов в контейнерах, но мы хотим, чтобы эти объекты были последовательными и были максимально удобными для кеширования. Также вероятно, что вы хотите отслеживать не все объекты в контейнере, а лишь некоторые из них. В этом случае ваша реализация имеет некоторые недостатки. Главный из них — вы храните std::unique_ptr вместе с каждым объектом, поэтому они больше не являются последовательными. Считайте, что у вас был:

std::vector<T> vec;

Тогда в памяти у вас есть:

T0 T1 T2 T3 ...

Но теперь вы хотите отследить некоторые из Ts, то вы должны написать:

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 * — это ключ, разве вы не потеряете ключ при перемещении объекта?

    — Барнак

  • 1

    Я добавил пример. Хитрость заключается в том, чтобы обновлять и ключ, и значение на карте при перемещении объекта.

    — Г. Сон

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

Более того, вы не можете переместить объект, когда его использует другой поток. И в настоящее время нет возможности даже проверить его, используется ли он или что-то в этом роде. Чтобы исправить это, вам нужно блокировать мьютекс каждый раз, когда вы его используете или перемещаете. Это означает, что перемещение может занять много времени из-за длительного ожидания, и если у вас их много, вам придется заблокировать такое количество мьютексов — что является хорошим источником взаимоблокировок, если вы не применяете специальные алгоритмы, которые гарантируют, что мьютексы будет заблокирован безопасным способом.

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

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *