5
\$\begingroup\$

Below is an implementation of a shared_any:

#include <typeindex>
#include <cstddef>
#include <utility>
#include <compare>

class shared_any
{
public:
    shared_any() noexcept = default;

    template<class T, class ... Args>
    explicit shared_any(std::type_identity<T>, Args&&... args):
        m_holder{std::type_identity<T>{}, std::forward<Args>(args)...}
    {}

    ~shared_any()
    { reset(); }

    shared_any(shared_any&& other) noexcept:
        m_holder{std::exchange(other.m_holder, holder{})}
    {}

    shared_any& operator=(shared_any&& other) noexcept
    {
        reset();
        m_holder = std::exchange(other.m_holder, m_holder);
        return *this;
    }

    shared_any(shared_any const& other):
        m_holder{other.m_holder}
    { m_holder.inc_usecount(); }

    shared_any& operator=(shared_any const& other)
    {
        shared_any tmp{other};
        *this = std::move(tmp);
        return *this;
    }

    template<class T>
    T* get_if() const noexcept
    { return m_holder.get_if<T>(); }

    void reset() noexcept
    {
        m_holder.dec_usecount();
        m_holder = holder{};
    }

    auto operator<=>(shared_any const&) const = default;

    auto use_count() const
    { return m_holder.usecount!=nullptr? *m_holder.usecount : static_cast<size_t>(0); }

private:
    static void noop(void*) {}

    struct holder
    {
        holder() = default;

        template<class T, class ... Args>
        explicit holder(std::type_identity<T>, Args&&... args):
            pointer{new T(std::forward<Args>(args)...)},
            current_type{std::type_index{typeid(T)}},
            usecount{new size_t(1)},
            destroy{[](void* obj){ delete static_cast<T*>(obj);}}
        {}

        auto operator<=>(holder const& other) const noexcept
        { return std::compare_three_way{}(pointer, other.pointer); }

        void inc_usecount() noexcept
        { ++(*usecount); }

        void dec_usecount() noexcept
        {
            if(usecount != nullptr)
            {
                --(*usecount);
                if(usecount == 0)
                {
                    destroy(pointer);
                    delete usecount;
                }
            }
        }

        template<class T>
        T* get_if() const noexcept
        {
            if(current_type == std::type_index{typeid(T)})
            { return static_cast<T*>(pointer); }
            return nullptr;
        }

        void* pointer = nullptr;
        std::type_index current_type = std::type_index{typeid(void)};
        size_t* usecount = nullptr;
        void (*destroy)(void*) = noop;
    };

    holder m_holder;
};

For now, I do not need any thread-safe ref-counting. Also I realize that I potentially have a leak in case one of the two new expressions in holder throws an exception, though new size_t should never throw in practice.

  • What is the cleanest way to fix the potential memory leak?
  • Are there any other issues that I have not thought of?

Smoke-testing seems fine: https://gcc.godbolt.org/z/7zz499aaa

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Why not just use std::shared_ptr<std::any>? I don't believe there's any need for user counting outside of multithreading where it is important. \$\endgroup\$
    – ALX23z
    Commented May 28 at 18:48
  • 1
    \$\begingroup\$ @ALX23z. This becomes a pointer to a pointer. \$\endgroup\$
    – user877329
    Commented May 28 at 19:44

1 Answer 1

7
\$\begingroup\$

Reuse std::shared_ptr

The reason to make a shared_any is to avoid the double pointer indirection from std::shared_ptr<std::any>. However, you can still make use of std::shared_ptr. For example, you could use a std::shared_ptr<void> with a custom deleter. Then you just need to add the "any" part somehow.

Of course you can only store one pointer in a std::shared_ptr, but you need that to point to the object itself, as well as to the type tag. To solve this issue, consider creating a base class for tagged objects that just contains the type tag, and a templated derived class that stores the actual type:

struct TaggedObjectBase {
    std::type_index tag;
};

template<typename T>
struct TaggedObject: TaggedObjectBase {
    T object;

    template<typename... Args>
    TaggedObject(Args&&... args):
        TaggedObjectBase{std::type_index{typeid(T)}}, 
        object{std::forward<Args>(args)...} {}
};

The trick is that you can store a pointer to a derived class in a std::shared_pointer to a base class. So a very basic class using this could look like:

struct SharedObject {
    std::shared_ptr<TaggedObjectBase> pointer;

    template<typename T>
    SharedObject(T&& t):
        pointer(std::make_shared<TaggedObject<T>>(std::forward<T>(t))) {}

    template<typename T>
    T* get_if() {
        if (pointer && pointer->tag == std::type_index{typeid(T)}) {
            return &std::static_pointer_cast<TaggedObject<T>>(pointer)->object;
        } else {
            return nullptr;
        }
    }
    …
};
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.