0

I was recently introduced to this mechanism of using a std::unique_ptr to implement a "generic" RAII mechanism:

// main.cpp

#include <memory>
#include <sys/fcntl.h>
#include <unistd.h>

#define RAII_CAT(x) raii ## x

#define RAII_CAT2(x) RAII_CAT(x)

#define RAII_FD(X) \
    std::unique_ptr<int, void(*)(int*)> RAII_CAT2(__LINE__){X, [](int* x){ if (-1 != *x) { close(*x); }}}

int main(int argc, char* argv[]) {
  {
    int fd = open("foo.txt", O_RDONLY);
    RAII_FD(&fd);
  }

end:
  return 0;
}

In the above code, the RAII_FD macro creates a std::unique_ptr object whose custom deleter takes an int* -- the pointer to a file-descriptor -- and calls close() on the file-descriptor.

I like this mechanism a lot, but I have a minor gripe that the custom deleter requires a pointer as its argument: aesthetically it feels a bit less than desirable.
E.g. in the code above, the custom deleter is a thin wrapper over int close(int) -- it would be nice, therefore, if the custom deleter could take an int instead of an int*...in which case perhaps a wrapper function wouldn't be necessary at all: perhaps a function pointer to int close(int) itself could be supplied.

I.e. variations of the following were tried, trying to register a custom deleter with signature void func(int) instead of void func(int*):

// main.cpp

#include <memory>
#include <sys/fcntl.h>
#include <unistd.h>

#define RAII_CAT(x) raii ## x

#define RAII_CAT2(x) RAII_CAT(x)

#define RAII_FD(X) \
    std::unique_ptr<int, void(*)(int)> RAII_CAT2(__LINE__){X, [](int x){ if (-1 != x) { close(x); }}}

int main(int argc, char* argv[]) {
  {
    int fd = open("foo.txt", O_RDONLY);
    RAII_FD(fd);
  }

end:
  return 0;
}

...the compile error was a vomit of stl errors that I perhaps don't grok 100%, but I think the gist is there are int*/int mismatches in the various template expansions.

Is there another similar mechanism by which one can implement a "generic" RAII mechanism with custom deleters whose argument doesn't necessarily need to be a pointer?

I'm open to learning about all possible solutions, but solutions that are actually usable in my target environment must be C++11 and non-Boost. Most preferable would be some similar "thin wrapper" over STL-native objects.

StoneThrow
  • 5,314
  • 4
  • 44
  • 86
  • 3
    It's true that `std::unique_ptr` is designed to manage a _pointer_, but you could also create your own very simple class that performs some function when it is destroyed. – Drew Dormann Feb 18 '23 at 20:46
  • You mean like a class? – Kenny Ostrom Feb 18 '23 at 20:46
  • @DrewDormann - agreed, and that was my original idea before being introduced to the noted "mechanism". I thought the possibility of using STL-native objects only might be preferable to writing my own class, which led me down this road of inquiry. – StoneThrow Feb 18 '23 at 20:48
  • 1
    Hopefully, you also recognize that you are using fragile C code that lacks RAII. If you had used `std::ifstream`, the RAII would be correctly engineered into the type. – Drew Dormann Feb 18 '23 at 20:55
  • @RichardCritten - definitely not a problem, functionally, agreed. It's just a very minor inquiry over the _aesthetics_. Most people know the functions `int open(int)` and `int close(int)`, i.e. they are used to dealing with the file-descriptors as integers, so the need to deal with file descriptor _pointers_ just becomes unnecessarily noteworthy while reading code. – StoneThrow Feb 18 '23 at 20:55
  • @DrewDormann - agreed again RE: `std::ifstream`. The use of file-descriptors was only to provide a simple example for discussion. There are other `C` objects I have to work with that don't have such RAII-enabled wrappers, e.g. `magic_t`. You'll note that the `std::unique_ptr` provides a nice, reusable solution that is relatively "thin" because it only needs a lambda function -- it "feels"/"reads" as more lightweight than implementing indivudual RAII wrapper classes. – StoneThrow Feb 18 '23 at 20:57
  • 2
    You appear to be reinventing [`scope_exit`](https://en.cppreference.com/w/cpp/experimental/scope_exit). More commonly known as [Scope guard](https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Scope_Guard). If you search for that, there are many implementations of the idea. – Igor Tandetnik Feb 18 '23 at 21:01
  • Solving this problem without using `#define` would also make your "vomit of errors" much more manageable. – Drew Dormann Feb 18 '23 at 21:06
  • Implementations of the Guidelines Support Library (GSL) also have [`finally`](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#gslutil-utilities) for adding (almost) RAII in legacy C code bases. See [Microsofts GSL](https://github.com/microsoft/GSL/blob/main/docs/headers.md#non-member-functions-2) and [gsl-lite](https://github.com/gsl-lite/gsl-lite) for example. – paleonix Feb 18 '23 at 21:29
  • @IgorTandetnik - although `std::experimental::scope_exit` appears to be a C++17 feature, it is indeed leading me to relatively thin implementations that can be accomplished in C++11 -- thank you. – StoneThrow Feb 18 '23 at 22:06

1 Answers1

3

std::unique_ptr doesn't in fact operate only on native raw pointers. It supports all types satisfying the NullablePointer requirements. Basically the type is supposed to behave like a scalar type with respect to value semantics and equality comparison and be nullable with nullptr into a distinct null state that is also the value-initialized state. This requires that you have a state to represent the null state, which in your case can be served by -1.

Such a type would be provided to unique_ptr as a type member pointer of the custom deleter:

// Code not tested!
// You must check that I didn't make a stupid mistake or forgot a requirement!

struct Deleter {
    struct pointer {
        int fd = -1;

        pointer() noexcept = default;
        pointer(std::nullptr_t) noexcept {}

        // should probably be explicit
        // but then `fd` cannot be passed directly to the
        // std::unique_ptr constructor
        pointer(int fd) noexcept : fd(fd) {};

        friend bool operator==(pointer p, pointer q) noexcept {
            return p.fd == q.fd;
        }

        // operator!= is optional with C++20
        friend bool operator!=(pointer p, pointer q) noexcept {
            return !(p == q);
        }

        operator bool() noexcept { return *this != nullptr; }
    };

    // unique_ptr calls this only if pointer doesn't represent the null state
    void operator()(pointer x) noexcept { close(x.fd); }
};

Then you can use

std::unique_ptr<int, Deleter> u(fd);

in the way you intent it to. In fact it doesn't really matter that the lement type is int. std::unique_ptr<double, Deleter> would work as well. However, you can't actually dereference with *u. If that is required, then you need to give pointer a operator* as well and in that case the element type should match the type returned by it.

Whether this is really useful given the amount of boilerplate code and the somewhat unusual interpretation of pointer is up to you.

user17732522
  • 53,019
  • 2
  • 56
  • 105
  • Very enlightening answer, from which I newly learned this concept that `std::unique_ptr` doesn't require _pointers_ per se, but this more generic concept of any type that can be constructed to obey the "NullablePointer" requirements -- fascinating and educational! – StoneThrow Feb 18 '23 at 22:08