lesenk
lesenk

Reputation: 805

Proper encapsulation with class relations when creating objects

I’m writing for practice a C++ wrapper over libusb. I want my API to fully hide the implementation of the underlying lib: the user should not even know I’m actually using libusb. So I created two classes: Context and Device. A Device is created from a Context using the open method.

So:

//--- context.hpp -------------------
class Context final
{
public:
  Context();
  ~Context();

  std::unique_ptr<Device> open(uint16_t vid, uint16_t pid);

private:
  struct Impl;
  std::unique_ptr<Impl> impl;
};

//--- context.cpp -------------------
struct Context::Impl
{
  libusb_context* ctx;
};

Context::Context()
  : impl(std::make_unique<Impl>())
{ /* ... */ }

//--- device.hpp -------------------
class Device final
{
public:
  Device();
  ~Device();

private:
  struct Impl;
  std::unique_ptr<Impl> _impl;
};

//--- device.cpp -------------------
struct Device::Impl
{
  libusb_device_handle* handle;
}

Device::Device()
  : _impl(std::make_unique<Impl>())
{}

Now the question is: how do I implement the open method? I need to call libusb_open_device_with_vid_pid in the implementation which will take the libusb_context* of my Context::Impl and store the handle in Device::Impl. Here are options I thought:

  1. I create a constructor of Device taking a pointer to Context, but the ctor of Device does not have access to the libusb_context* pointer to call the function;
  2. I follow point number one and put the Context::Impl in a header and make Device a friend of Context: this sounds ugly though;
  3. I create a constructor of Device that takes a libusb_context* pointer: but I’d be breaking the encapsulation, the user should’nt see the libusb details;
  4. I add a native_handle method to Context so I can do number one and get the implementation pointer but causing the same problem as number three.
  5. I make the function call in open but how do I initialize my Device with the libusb_device_handle* I get? I can’t have a ctor of Device taking such a pointer, breaking the encapsulation.

Upvotes: 2

Views: 122

Answers (1)

Krzysztof Kosiński
Krzysztof Kosiński

Reputation: 4325

The friend-based solution is in fact the cleanest, it's what friend was designed for.

// device.hpp
class Device final
{
public:
  ~Device();

private:
  Device();
  struct Impl;
  std::unique_ptr<Impl> impl;
  friend class Context;
};

// device.cpp
struct Device::Impl
{
  libusb_device_handle* handle;
  Impl() : handle(nullptr) {}
}

Device::Device() : impl(new Device::Impl()) {}

std::unique_ptr<Device> Context::open(uint16_t vid, uint16_t pid) {
  std::unique_ptr<Device> result(new Device());
  result->impl->handle = libusb_open_device_with_vid_pid(vid, pid);
  return result;
}

Note that you can actually return Device rather than std::unique_ptr<Device> and treat all your bindings as value objects to avoid the extra level of indirection.

EDIT: another option is to use a void pointer. This removes the friend declaration at the expense of introducing a potentially unsafe cast.

// device.hpp
class Device final
{
public:
  ~Device();

private:
  Device(void *handle);
  struct Impl;
  std::unique_ptr<Impl> impl;
};

// device.cpp
struct Device::Impl
{
  libusb_device_handle* handle;
  Impl(void *handle)
    : handle(static_cast<libusb_device_handle*>(handle)) {}
}

Device::Device(void *handle)
  : impl(new Device::Impl(handle)) {}

std::unique_ptr<Device> Context::open(uint16_t vid, uint16_t pid) {
  void *handle = libusb_open_device_with_vid_pid(vid, pid);
  std::unique_ptr<Device> result(new Device(handle));
  return result;
}

Upvotes: 3

Related Questions