1

I had the idea to use std::unique_ptr as a base class for another class that manages some kind of external resource.

Let's assume my program needs to manage resources provided by a C-like library via the following functions: R* lib_create_resource(A* allocator); and void lib_destroy_resource(R* resource, A* allocator);. I would like to create a class that manages this resource, so I thought that using std::unique_ptr as a base class would be a good idea. Here's a pseudo-implementation:

struct Resource: public std::unique_ptr<R, std::function<void(R*)>> {
    Resource(): 
        unique_ptr{
            lib_create_resource(&my_allocator),
            [this](R* r) { lib_destroy_resource(r, &my_allocator); }
        }
    { }

    /* Other functions that manipulate the resource */

private:
    A my_allocator;
};
  • Why am I using std::unique_ptr as a base class rather than as a non-static member? Because I would like to expose std::unique_ptr's methods, such as get(), reset(), operator bool(), etc., and this way I don't need to manually re-define each of them, especially when the library provides many kinds of different resources instead of just one, and I want to write a separate wrapper for each of them.
  • Why not use std::unique_ptr<R, std::function<void(R*)>> on its own then, without the Resource class? Because I would like to extend std::unique_ptr and provide additional methods, specific to this type of resource.

Now, the above pseudo-implementation has two major problems, which are the main point of my question.

  1. Since base classes are initialised before non-static members, my_allocator is passed to lib_create_resource() uninitialised. This isn't hard to fix, as I can just default-initialise the unique_ptr base and re-assign it in the constructor's body, but I think it's easy to forget about this.
  2. Similarly, during destruction, the non-static members are destroyed before the base classes. First, my_allocator will be destroyed, then ~unique_ptr() will be called, which in turn will call lib_destroy_resource() with my_allocator. But at that point, my_allocator no longer exists.

I haven't been able to come up with any solution for issue number 2. Is there a way to re-design this class so that lib_destroy_resource() doesn't access my_allocator outside its lifetime? Of course, one solution would be to manually call lib_destroy_resource() or std::unique_ptr::reset() at the appropriate times, but automatic resource management with RAII is considered good practice, especially when exceptions are involved. So is there a better way to accomplish this, possibly by implementing my own std::unique_ptr-like class?

md1357
  • 425
  • 2
  • 8
  • 2
    While a resource might be unique, I don't really seeing it having an `is-a` relationship with `unique_ptr`. Why not just have `Resource` contain a `std::unique_ptr>` member? – NathanOliver Apr 21 '22 at 12:37
  • 1
    `I had the idea to use std::unique_ptr as a base class for another class that manages some kind of external resource.` - bad idea. `unique_ptr` should not be inherited. – Marek R Apr 21 '22 at 12:37
  • 3
    inheritance isnt the solution to everything. Often it creates more problems than it solves. You want to inherit to avoid reimplementing a couple of methods. I suggest to reconsider if this is really worth it. – 463035818_is_not_an_ai Apr 21 '22 at 12:41
  • `How to design a RAII wrapper to be used as a base class?`, `Is there a way to re-design this class` So which are you trying to re-design? Are you trying to re-design the RAII wrapper that is to be used as a base, or the class that uses the RAII wrapper as a base? – eerorika Apr 21 '22 at 12:47
  • @eerorika I see this is a bit ambiguous, I meant either of them, or both of them, whichever would be easier to do. But judging by the other comments, it seems it might not be possible to do this the way I imagined it. – md1357 Apr 21 '22 at 12:53
  • With the inheritance way, `struct Resource: private A, public std::unique_ptr> {..};`... – Jarod42 Apr 21 '22 at 12:55
  • Does this answer your question? [How do I use a custom deleter with a std::unique\_ptr member?](https://stackoverflow.com/questions/19053351/how-do-i-use-a-custom-deleter-with-a-stdunique-ptr-member) – Öö Tiib Apr 21 '22 at 12:59
  • @ÖöTiib Thank you for the suggestion, but I don't see how that question is related to my question, I think I know how to use custom deleters with `std::unique_ptr`. My original rationale for not using `std::unique_ptr` as a member as opposed to base (also what @NathanOliver suggested) is explained in the question, but I see now that it might not have been a great idea. – md1357 Apr 21 '22 at 13:04
  • @MarekR Your comment also made me wonder - what's wrong with inheriting from `std::unique_ptr`, other than the problem that I have here, and the potential issue of non-virtual destructors? – md1357 Apr 21 '22 at 13:12
  • 1
    *"what's wrong with inheriting from `std::unique_ptr`"* Inheriting for any 3rd party library has the issue that you rely on existing but also future interface. Wrapping in your own type allows to have fixed interface. – Jarod42 Apr 21 '22 at 14:17
  • 1
    It appears that you could just use a `std::unique_ptr` (with a custom deleter), and provide the additional functions as free-standing functions rather than having to be member functions. – Eljay Apr 21 '22 at 14:38

1 Answers1

2

Ignoring other problems with such inheritance, 2. is easy to fix in analogous way to how you fixed 1. Reset the base in the destructor body:

~Resource() {
    reset();
}

This way the deleter is called before any member is destroyed.

Because I would like to expose std::unique_ptr's methods, such as get(), reset(), operator bool()

One thing that you could do is a stateless base, that delegates such functions through CRTP. Something like this:

template <class Derived, class Delegate>
struct Raii
{
    Delegate& crtp_delegate()
    {
        return static_cast<Derived&>(*this).delegate();
    }

    constexpr typename Delegate::pointer
    get() const noexcept
    {
        return crtp_delegate().get();
    };

    constexpr void
    reset(typename Delegate::pointer ptr = Delegate::pointer()) noexcept
    {
        return crtp_delegate().reset(ptr);
    };

    constexpr explicit
    operator bool() const noexcept
    {
        return static_cast<bool>(crtp_delegate());
    }
};

struct Resource: public Raii<
        Resource, std::unique_ptr<R, std::function<void(R*)>>> {

protected:
    std::unique_ptr<R, std::function<void(R*)>>&
    delegate()
    {
        return ptr;
    }

private:
    A my_allocator;
    std::unique_ptr<R, std::function<void(R*)>> ptr;
};
eerorika
  • 232,697
  • 12
  • 197
  • 326
  • Thank you, as mentioned in the question, I know that this is possible, I was just curious if the resource management could be made fully automatic. But of course, this is one way to make my example work. – md1357 Apr 21 '22 at 13:09
  • After your edits, that's a pretty interesting solution, thank you. – md1357 Apr 21 '22 at 13:44