简体   繁体   中英

C++11 and later: shared_ptr for managing system resources provided by a low-level C library

For the last couple years I've predominantly been a C developer with a bit of Python coding here and there. With the amount of contradictory sources I'm struggling to figure out the correct approach to managing system resources in modern C++.

I'm the author of libgpiod - a library for controlling GPIOs via the character device (which is the new way of managing GPIOs from userspace, as opposed to now deprecated sysfs interface). The core library code is written in C but I plan to provide bindings for other languages starting with C++11.

I don't want to get into much detail on what GPIOs are but in general a GPIO line is a configurable pin we can control associated with a GPIO chip which usually exposes several lines. The C library models this two-level hierarchy with two core structures: struct gpiod_chip and struct gpiod_line . Both are only visible to users as opaque pointers manipulated with provided API functions. Internally, the chip is associated with an open file descriptor (the device file residing in /dev/ ) and a couple variables containing the object state.

A pointer to an allocated chip object is returned to the user from one of the gpiod_chip_open() variants. The user is responsible for freeing the allocated resources with gpiod_chip_close() . As with most low-level C code, the user is tasked with managing the resource handle . The library is explicitly made thread-aware (in that there's no global state) but not thread-safe.

The chip manages the resources for all line objects associated with it, so my question below is only relevant for chips.

Now from what I've read so far, in modern C++ the new and delete operators should not generally be used manually. My initial idea for a chip class was thus:

namespace gpiod {

class chip {

    // [snip!]

private:

    std::shared_ptr<::gpiod_chip> _m_chip;

};

}

Which would make the chip class hold a reference to the gpiod_chip object. Copy & move constructors and the assignment operator would simply make use of the shared_ptr's reference counting to allow to freely move and copy the chip object around. When the reference count drops to 0, a custom deleter would call gpiod_chip_close() on the C object.

But then I noticed that some people recommend using a factory in such case and letting the users wrap the object in a smart pointer of their choice themselves.

Any advice on what the correct approach for my use case would be as far as modern C++ goes?

It is not necessary to use std::shared_ptr unless you need the resource to have shared ownership and it appears that in your example that's not the case. Most of the time it is only necessary to use std::unique_ptr which will take ownership of a pointer in its constructor then destroy the pointer with delete in its destructor. You say your library is implemented mostly in C so the pointer returned by gpiod_chip_open() is probably allocated with malloc so destroying it with delete which be undefined behavior. To solve this, you can specify a custom deleter functor for std::unique_ptr will call gpiod_chip_close() its destructor rather than delete .

You can do something like this:

#include <memory>

struct gpiod_chip_deleter {
    void operator()(::gpiod_chip* chip) noexcept {
        ::gpiod_chip_close(chip);
    } 
};

using gpiod_chip_ptr = std::unique_ptr<::gpiod_chip, gpiod_chip_deleter>;

// ...

gpiod_chip_ptr chip(::gpiod_chip_open());

// ...

You should use std::shared_ptr only if you actually need shared ownership semantics, which tends to be rare.

A pointer to an allocated chip object is returned to the user from one of the gpiod_chip_open() variants. The user is responsible for freeing the allocated resources with gpiod_chip_close()

The C API does not seem to provide shared ownership. There's no need for you to add it, is it?

You should instead use std::unique_ptr with a custom deleter or write your own small wrapper class with move semantics, which has a similar effect but provides a more problem-specific interface. Or you implement such a wrapper class in terms of a private std::unique_ptr with a custom deleter. The key is really to utilise move semantics .

Here is an example to understand what that means:

class Chip final
{
public:
    Chip() :
        ptr(gpiod_chip_open())
    {
        // throw if gpiod_chip_open reported an error
    }

    // add other constructors for additional variants of gpiod_chip_open()

    ~Chip()
    {
        gpiod_chip_close(ptr); // add nullptr check if gpiod_chip_close requires one
    }

    Chip(Chip const&) = delete;
    Chip& operator=(Chip const&) = delete;

    Chip(Chip&& other) :
        ptr(other.ptr)
    {
        other.ptr = nullptr;
    }

    gpiod_chip* Get()
    {
        return ptr;
    }

private:
    gpiod_chip* ptr;
};

That's basically all you need to start. Add other operations (eg a move assignment operator) if you need it. You may also want to get rid of the Get member function and completely wrap all other API functions inside of the class; it depends on the level of wrapping you require.

The library is explicitly made thread-aware (in that there's no global state) but not thread-safe.

Correct synchronisation is an entirely different problem, which std::shared_ptr could not magically make go away in any case.

All which std::shared_ptr guarantees is that the shared-ownership mechanics themselves will work in a multi-threaded context - ie that the reference count is safely incremented and decremented and that no multiple deletions occur if the reference count reaches zero.

It does not guarantee that the managed object itself can safely be used from multiple threads. Whether that's safe or not depends on the managed object. If a gpiod_chip (that is, its operations) cannot safely be used from multiple threads, then neither could a std::shared_ptr<gpiod_chip> .

You would practically have safe access to a gpiod_chip* in both thread A and thread B pointing to the same gpiod_chip , but you'd still need to synchronise operations on that gpiod_chip .

I don't know your exact requirements, but is there any problem in making chip non-copyable, just movable?

class chip {
  int* handle;
public:
  chip(const chip&) = delete;
  chip& operator=(const chip&) = delete;

  chip(chip&& c) { operator=(std::move(c)); }
  chip& operator=(chip&& c) {
    handle = c.handle;
    c.handle = nullptr;
    return *this;
  }

  chip() : handle(gpiod_chip_open()) { }

  ~chip() { gpiod_chip_close(handle); }
};

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM