c++ – “Smart pointer” meant to stay updated when the pointed object is moved in memory

I wasn’t sure about how to name it, maybe “follow_ptr”, “self_updating_ptr”, or “stalking_ptr” or something on those lines. For now it’s called Identifier.

What I’m trying to achieve is a pointer wrapper which will always refer to the same object even when that object is moved in memory (vector resizes is a quite frequent example, also algorithms like std::remove_if that can move elements around).

A requirement is that the object is stored within an “Identified” class. That class is necessary to keep all the Identifiers updated.

The trick is having a double indirection, where a raw pointer living in the heap will point to the object to be stalked:

#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};
    };

The Identifier, or stalker, acts as an in-between a smart pointer and an optional. The idea is that if Identifiers outlive an object, they’re still valid (assuming the user checks with has_value before using them, like with an optional).

I’m unsure if I should just delete the default constructor, so that it’s always certain that an Identifier’s pointer to the Inner_identifier is always valid, and I can get rid of some checks. For now I’ve left it just to make writing the example simpler.

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(); }
    };

Finally the Identified class, which holds the instance of the object to be pointed to by one or more Identifiers. It is responsible for updating the Inner_identifier whenever it is moved around in memory with either move constructor or move assignment. On the opposite the copy constructor makes sure that the new copy has its own new Inner_identifier and all the existing Identifiers still work with the instance being copied from. Upon destruction, the Inner_identifier is nullified but it will keep existing for reference as long as at least one Identifier to the now defunct object still exists (hence the internal 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;
    };

On top of criticisms, I’d like some advice on naming. If I were to call the Identifier “follow_ptr”, “self_updating_ptr”, or “stalking_ptr”, I’ve no idea how to call the other two classes.

Aside for the first capital letter of the classes, does the interface feel “standard” enough?

Here is an usage example, compile in debug mode for the exceptions:

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; }
    }