2

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.
2
  • Does the user need to see a context object at all? Could that just be a hidden implementation detail of Device? I'm not familiar with libusb but often context type objects are a C handle to maintain instance information. In C++ that same job is often done internal to the class - the class object is its own handle. Commented Oct 8, 2016 at 0:19
  • @Galik There can be multiple devices per context, and multiple contexts. Commented Oct 8, 2016 at 1:38

1 Answer 1

3

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;
}
Sign up to request clarification or add additional context in comments.

4 Comments

Despite it already uses friendship, wouldn't it still be better to return std::unique_ptr<Device>(new Device(libusb_open_device_with_vid_pid(vid, pid)));?
Initializing attributes of Device inside the Context class feels wrong, isn't it? Shouldn't it be done by a Device ctor taking the Context as parameter and make the function call? The friendship would be the other way around: Device a friend of Context. Also, if I use objects instead of pointers, how can I pass the object to a function without copying it (wouldn't make sense) and without invalidating the caller argument by moving it? Maybe pass a reference?
If you don't want to directly initialize the attribute, you can pass the libusb handle to the constructor using a void pointer. I updated the answer.
Setting up the friendship the other way around would be rather confusing. The above implementation of open() follows a pattern known as a factory method: you use a method on an object to create some other object related to it. Still, if you want to do it the other way, you can use const Context & as the parameter type - this will pass an object without copying it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.