I'm trying to implement something similar to shared_ptr except that the object is does not need to be allocated on the heap. Basically all the devices are populated to std::unordered_map per device type, as there may be multiple devices of the same type. The device should live as long as it's used by the "user". Similar case as shared_ptr that prolongs the object's lifetime. I could just use std::shared_ptr however I do not necessarily want to allocate memory on the heap for every device.
Currently I see 4 solutions:
- Use std::shared_ptr and just accept the fact that the heap will be used for every device object.
- Use std::shared_ptr and perhaps use some custom allocator (perhaps something with std::pmr::polymorphic_allocator).
- Perhaps try to use boost::intrusive_ptr?
- Develop own ref counting solution.
I decided to give the last one a try (just very basic draft) just to see if it makes any sens and the resulting code you can find below. I would be grateful if you could share your thoughts about the code as share your own ideas (if you have any).
I'm generally not against std::shared_ptr, I just have a feeling that they are getting kinda overused. I personally use them e.g. for the purpose of async operations in Boost::Asio, but in this case I'm just curious if it make sense to use something different.
Compiled with GCC 15, std=C++23
#include <unordered_map>
#include <iostream>
#include <atomic>
#include <cassert>
template <typename T>
struct Handle
{
explicit Handle(std::string id_, T&& device_)
: id{std::move(id_)}
, device{std::move(device_)}
, ref{0}
{}
std::string id{};
T device{};
std::atomic<std::size_t> ref{};
T* operator-> ()
{
return &device;
}
};
template <typename T>
static std::unordered_map<std::string, Handle<T>> device_map;
template <typename T>
void handle_inc_ref(Handle<T> *handle)
{
std::cout << "Incrementing ref\n";
++handle->ref;
}
template <typename T>
void handle_dec_ref(Handle<T> *handle)
{
std::cout << "Decrementing ref\n";
if (--handle->ref == 0) {
std::cout << "Deleting handle\n";
device_map<T>.erase(handle->id);
} else {
std::cout << "Not deleting handle\n";
}
}
template <typename T>
struct View
{
Handle<T>* handle_ {nullptr};
View() = default;
View(Handle<T>* handle)
: handle_{handle}
{
handle_inc_ref(handle_);
}
View(View&& other) noexcept
: handle_{other.handle_}
{
other.handle_ = nullptr;
}
View(const View&) = delete;
View& operator=(const View&) = delete;
View& operator=(View&& other) noexcept
{
if (this != &other) {
handle_dec_ref(handle_);
handle_ = other.handle_;
other.handle_ = nullptr;
}
return *this;
}
~View()
{
if (handle_) {
handle_dec_ref(handle_);
}
}
T* operator->()
{
return &handle_->device;
}
operator bool()
{
return handle_ != nullptr;
}
std::size_t use_count() const
{
return handle_->ref.load();
}
};
template <typename T>
void add(std::string id, T device)
{
device_map<T>.emplace(std::piecewise_construct,
std::forward_as_tuple(id),
std::forward_as_tuple(std::move(id), std::move(device)));
}
template <typename T>
View<T> get(const std::string& id)
{
auto elem = device_map<T>.find(id);
if (elem == device_map<T>.end()) {
return View<T>{};
}
return View<T>{&elem->second};
}
struct Led
{
void on()
{
}
void off()
{
}
};
struct Sensor
{
float get_value()
{
return 123;
}
};
int main()
{
/* Devices populated somewhere */
add("led@1", Led{});
{
auto led = get<Led>("led@1");
assert(led);
led->on();
}
}