1

I am designing and prototyping an ncurses C++ wrapper. It's a hobby project, nothing too serious. I will omit include guards and stdlib includes etc.

Consider the following files:

application.hpp

#include "ncurses_forward_decl.hpp"
#include "window/window.hpp"

#define PPCURSES_VERSION_MAJOR 0
#define PPCURSES_VERSION_MINOR 3
#define PPCURSES_VERSION_PATCH 0

namespace ppc {

enum Focus { Canvas = -1 };

using WindowPtr = std::shared_ptr<Window>;

class Application {
 public:
  Application();
  ~Application();

  WindowPtr NewWindow(Point pos, Point size);

  // Event GetWindowEvents();
  char GetKeyEvents();
  void Draw();

 private:
  int focus_id_;
  std::unique_ptr<WINDOW> canvas_;
  std::vector<std::shared_ptr<Window>> windows_;
};

}  // namespace ppc

window.hpp

#include "ncurses_forward_decl.hpp"
#include "util/point.hpp"
#include "widgets/widget.hpp"

namespace ppc {
// FIXME: hide this deleter from public API
void RawWindowDeleter(WINDOW *win);

using WidgetPtr = std::shared_ptr<Widget>;

class Window {
  friend class Application;
  friend class Widget;

 public:
  Window(Point pos, Point size);

  template <class T, class... Args>
  void AddWidget(Args... args) {
    WidgetPtr widget = std::make_shared<T>(args...);
    widgets_.push_back(widget);
  }

  void SetPosition(Point t_position);
  inline Point Position() { return position_; }

  void SetSize(Point t_size);
  inline Point Size() { return size_; }

 protected:
  Point position_;
  Point size_;
  std::shared_ptr<WINDOW> curses_window_;

  inline WINDOW *RawPtr() { return curses_window_.get(); }
  void Draw();
  char GetKeyEvents();

 private:
  std::vector<WidgetPtr> widgets_;
};

}  // namespace ppc

widget.hpp

#include "ncurses_forward_decl.hpp"

namespace ppc {
class Window;

class Widget {
  friend class Window;

 public:
  virtual ~Widget() = default;

 protected:
  WINDOW *RawPtr(Window &win);
  virtual void Draw(Window &win) = 0;
};

}  // namespace ppc

label.hpp

#include "util/point.hpp"
#include "widget.hpp"

namespace ppc {

class Label : public Widget {
  friend class Window;

 public:
  Label(Point pos, const char *text);

  Point position;
  std::string text;

 protected:
  void Draw(Window &win) override;
};

}  // namespace ppc

Application can create Windows, which in turn can create and manage Widgets (such as a Label). But, for the Widgets to be able to Draw() themselves, they need an ncurses' raw pointer WINDOW *, which is obtained via the RawPtr() method, but I'm trying to hide it from the public API. Widget's RawPtr(Window &win) method returns the Window's raw pointer, becuase Widget is declared as a friend of Window. I'm also declaring Window as a friend of Label, and Application as a friend of Window. I feel like this is a bad decision that will lead to spaghetti later on, and it would be a lot easier to just expose RawPtr() in Window to the public. But I really don't want end users having anything to do with internal ncurses workings. Is there a design pattern I'm missing here?

3
  • 1
    codereview.stackexchange.com might be a better fit for tour question. :)
    – Andy
    Sep 10 '18 at 15:54
  • 3
    I'm voting to close this question as off-topic, because requests for improvements to code are better suited for Code Review, if they include all relevant code and are titled to state what the code does. Oct 10 '18 at 16:32
  • 1
    That's a lot of std::shared_ptrs. Are you sure non-owning pointers and std::unique_ptrs wouldn't work? Oct 10 '18 at 23:59
0

I believe the best answer is to make rawptr publicly accessible (but protected amounts to the same thing since u can subclass to access). And that is a good name.

Just document clearly to try to avoid using the method. Document that its low level and any rules your implementation imposes on its use.

This is the best compromise.

It allows extensions easily. And it avoids the long interconnected unmodular list of friends you would need to keep it private.

I don't see why label needs friend class window.

Rawptr method of widget should be static.

Not clear what rawwindowdeleter does.

Not clear why has friend class application.

0

Depending on your needs, and one of the things that had me answering this way is that you referred to "users" like they're not on your internal team, I tend to go with an unsafe API with safety imposed on top statically.

It tends to be a lot simpler to export simpler but maybe less safe functions for a dylib/shared lib because those are all, perhaps unfortunately, revolving around the low-level constructs of C and all the gory details of ABI like calling conventions and such with no inherent awareness of things like function overloading or destructors. If you want to maximize the range of compilers which users can use against your API without ABI issues, then it tends to help to be a bit more C-like in the "raw API".

Then if you want to impose safety back on top, you can provide a static library, for example, which wraps those less safe C or C-like interfaces into some RAII-conforming set of C++ wrappers (internally linked to the binaries of your users using your SDK/API). That is a pain and it's kind of jumping through hurdles to get back the interfaces that you originally wanted, but doing it in "two layers" like this will often maximize compatibility with the widest range of compilers (and FFIs if that's a target) and minimize the kinds of issues you might face in practice working across module boundaries.

These days I'm bouncing back and forth between C and C++ since I use C in the headers for the central SDK/APIs (in my case even FFI compatibility is a concern because users tend to use these APIs from languages like Java, C#, the internal scripting uses it from Lua, etc) and then C++ to simplify the implementation of those APIs. And because of the frequency at which I do this sort of interop, I've also adopted the convention of distinguishing .h for C API header files and .hpp for C++ headers.

This could all be overkill if you're just targeting some API to be used by people using the same compilers and so forth. I just proposed it because the way you described "users" here and doing things like avoiding standard library had me suspecting that you want to target the broadest audience possible, in which case you tend to do best by refraining from object constructs like constructors and destructors and even function overloading (name/symbol mangling issues) as well as avoiding throwing exceptions across module boundaries and even refrain from vtables and so forth for the "raw" exported functions. Absent a stub library you can imagine that even constructors and destructors (essential for smart pointers to work) would have to be invoked manually if symbols are looked up at runtime (ex: using dlsym on Linux) and used that way. But for anything with internal linkage to the users of your library, you can go all out with C++ goodies.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.