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).
EDIT: One requirement is to allow storing objects in sequential containers (like vector and deque) without losing sequential storage as one would by using unique_ptr or shared_ptr. This whole system is not meant to take care about ownership.
It's my bad for using the term "smart pointer in the original title", it's smart in the sense that it follows the pointed object as opposed to an observer pointer which wouldn't do that.
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:
#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; }
}
shared_ptr
and update it each time the tracked object is modified. \$\endgroup\$shared_ptr
with variables but how you described is more or less the same - only that you suggest that an external class manages the instances update which is probably better. \$\endgroup\$