0

I am trying to abstract the explicit creation and destruction of raw handles with the usage of classes. The actual handle is stored as a private class member (so that the user doesn't interact with the lower-level details), which is created on construction and destroyed on destruction. Is there a design pattern of some sorts that could help achieve what the code below is trying to accomplish?

Note that it is possible for there to be loads of classes interdependent to each other, therefore it would be both tedious and bad practice to pollute each class with lots of friend statements.

#include <memory>

// Handle types may vary
typedef uint32_t A_Handle;
typedef uint32_t B_Handle;
typedef int64_t C_Handle;

extern void createA(A_Handle*);
extern void destroyA(A_Handle);
extern void createB(B_Handle*);
extern void destroyB(B_Handle);
extern void createC(C_Handle*, A_Handle, B_Handle);
extern void destroyC(C_Handle, A_Handle, B_Handle);

class A
{
private:
    A_Handle handle_;
public:
    A()
    {
        createA(&handle_);
    }

    ~A()
    {
        destroyA(handle_);
    }

    A(const A&) = delete;

    A& operator=(const A&) = delete;
};

class B
{
private:
    B_Handle handle_;
public:
    B()
    {
        createB(&handle_);
    }

    ~B()
    {
        destroyB(handle_);
    }

    B(const B&) = delete;

    B& operator=(const B&) = delete;
};

class C
{
private:
    C_Handle handle_;
public:
    std::shared_ptr<A> a;
    std::shared_ptr<B> b;

    C(const std::shared_ptr<A>& a, const std::shared_ptr<B>& b)
        : a(a)
        , b(b)
    {
        // Error a->handle_ and b->handle_ is private
        createC(&handle_, a->handle_, b->handle_);
    }

    ~C()
    {
        // Error a->handle_ and b->handle_ is private
        destroyC(handle_, a->handle_, b->handle_);
    }

    C(const C&) = delete;

    C& operator=(const C&) = delete;
};

// ...

int main()
{
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    std::shared_ptr<C> c = std::make_shared<C>(a, b);

    // ...

    return EXIT_SUCCESS;
}
MaximV
  • 155
  • 11
  • 2
    Recommend observing the [Rule of Three (or five)](https://en.cppreference.com/w/cpp/language/rule_of_three) in classes `A` and `B`. – user4581301 Jun 11 '21 at 22:52
  • 1
    Is the massive amount of "`extern`"s accommodating a C interface? – Ted Lyngmo Jun 11 '21 at 23:01
  • 1
    @user4581301 or rule of zero. As this class deals with ownership we have at least delete unwanted members (by look of this those classes should not be copyable) – Swift - Friday Pie Jun 11 '21 at 23:22
  • *"what the code below is trying to accomplish"* -- using example code as your functional specification puts you at a huge disadvantage when it comes to implementing such a thing. It also greatly hampers the ability of others to find this question with a search. You should write out in words what the desired functionality is (like you did when you described construction and destruction). – JaMiT Jun 12 '21 at 01:58
  • @TedLyngmo yes precisely that – MaximV Jun 12 '21 at 19:52
  • 1
    @user4581301 Yes you are right in saying that the classes should be non-copyable (I have updated the code with deleted copy constructors and assignment operators). But I don't understand how the rule of zero would work as the handle would never get destroyed, due to no destructor. – MaximV Jun 12 '21 at 20:09
  • 1
    The rule of zero wouldn't apply to the classes as written, but once I have a resource requiring special handling being used by multiple classes, I'll wrap that resource in a fully RAII-compliant class and classes `A`, `B`, and `C` would use that class to that they can observe the Rule of Zero. Keep the classes observing Three and Five as close to the resource they're protecting as possible so other classes can be as stupid as possible. – user4581301 Jun 13 '21 at 05:54

2 Answers2

1

Is there a design pattern of some sorts that could help achieve what the code below is trying to accomplish?

Yes. It is called Resource Acquisition Is Initialization, or RAII for short. Your first attempt is in the right direction, but it is likely incomplete. A thing to potentially be concerned about is that typically it is an error to "destroy" a raw handle multiple times. Hence, you should establish a "class invariant" that as a post condition of every member function, no two instances of the class own the same raw handle. Your classes currently violate such invariant. Consider what happens when you make a copy of an instance. There is a rule of thumb called rule of five (previously rule of three) that will help establishing that invariant.

As for the private access and avoiding friends, a good solution is to provide a public getter:

class A
{
public
    A_Handle get_handle() { return handle; }

The member is still encapsulated and the users of the class will be unable to break the invariant since they cannot modify it.

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • Thanks for the clear explanation, I guess getters will be the only option. I am still unsure as to whether this is a good idea since the book [C++ coding standards](http://library.bagrintsev.me/CPP/Sutter.C%2B%2B%20Coding%20Standards.2005.pdf) chapter 42 (page 74) states that giving away internals should be avoided because although they may not be able to modify the handle, they can still use the handle to make changes to the object in which that handle points to (such as by destroying it manually). – MaximV Jun 12 '21 at 19:23
  • @MaximV Document that behaviour is undefined if the user does something like that. – eerorika Jun 12 '21 at 19:24
  • I think you pointed it out yourself in saying that "A thing to potentially be concerned about is that typically it is an error to "destroy" a raw handle multiple times", Moreover this is the reason why I am wrapping the instances in shared pointers: so that construction and destruction only happen once (per std::make_shared). – MaximV Jun 12 '21 at 19:47
1

You don't have to roll your own solution for this. Instead, you can use std::unique_ptr with a custom deleter which knows how to destroy the handle when the unique_ptr goes out of scope.

Here's an example, using FILE * as a 'handle':

#include <cstdio>
#include <memory>

int main ()
{
    FILE *f = std::fopen ("myfile", "r");
    if (f)
    {
        std::unique_ptr <FILE, decltype (&std::fclose)> upf (f, std::fclose);
        // do things with the open file
        // ...
        // file will be closed here, when upf goes out of scope
    }
}

If your handle is not a pointer type, you can cast it to and from a void * (most handles fit in a void *). For example:

#include <sys/stat.h>
#include <fcntl.h>
#include <cstdint>
#include <memory>

int main ()
{
    int fd = open ("myfile", O_RDONLY);
    if (fd >= 0)
    {
        std::unique_ptr <void, void (*) (void *)> upfd
            ((void *) (uintptr_t) fd, [] (void *fd) { close ((int) (uintptr_t) fd); });
        // do things with the open file
        // ...
        // file will be closed here, when upfd goes out of scope
    }
}

You can, of course, define type aliases for those complicated looking templates to make the code neater.

std::unique_ptr has some nice features, including a deleted copy constructor and a viable move constructor. Also, you can work a similar trick with std::shared_ptr if you need shared ownership semantics (aka reference counting).

Paul Sanders
  • 24,133
  • 4
  • 26
  • 48
  • Thanks, this means that now I can wrap the raw handles in `unique_ptr`'s with a custom deleter function, in which the classes can now hold, hence they can now follow the rule of zero. However, this doesn't entirely answer the question as to how I could access the private handles from the other classes while maintaining as much encapsulation as possible. – MaximV Jun 13 '21 at 12:37
  • Well, that's true, `std::unique_ptr` doesn't protect your handle in the way that you can do if you write your own class. OTOH, it is feature complete and comes for free. – Paul Sanders Jun 13 '21 at 20:46