keebus
keebus

Reputation: 1010

C/C++ API design dilemma

I have been analysing the problem of API design in C++ and how to work around a big hole in the language when it comes to separating interfaces from implementations.

I am a purist and strongly believe in neatly separating the public interface of a system from any information about its implementation. I work daily on a huge codebase that is not only really slow to build, mainly because of header files pulling a large number of other header files, but also extremely hard to dig as a client what something does, as the interface contains all sorts of functions for public, internal, and private use.

My library is split into several layers, each using some others. It's a design choice to expose to the client every level so that they can extend what the high level entities can do by using lower level entities without having to fork my repository.

And now it comes the problem. After thinking for a long while on how to do this, I have come to the conclusion that there is literally no way in C++ to separate the public interface from the details for a class in such a way that satisfies all of the following requirements:

  1. It does not require any code duplication/redundancy. Reason: it is not scalable, and whilst it's OK for a few types it quickly becomes a lot more code for realistic codebases. Every single line in a codebase has a maintenance cost I would much prefer to spend on meaningful lines of code.

  2. It has zero overhead. Reason: I do not want to pay any performance for something that is (or at least should!) be well known at compile time.

  3. It is not a hack. Reason: readability, maintainability, and because it's just plain ugly.

As far as I know, and this is where my question lies, in C++ there are three ways to fully hide the implementation of a class from its public interface.

  1. Virtual interface: violates requirements 1 (code duplication) and 2 (overhead).
  2. Pimpl: violates requirements 1 and 2.
  3. Reinterpret casting the this pointer to the actual class in the .cpp. Zero overhead but introduces some code duplication and violates (3).

C wins here. Defining an opaque handle to your entity and a bunch of function that take that handle as the first argument beautifully satisfies all requirements, but it is not idiomatic C++. I know one could say "just use C-style while writing C++" but it does not answer the question, as we are speaking about an idiomatic C++ solution for this.

Upvotes: 1

Views: 1498

Answers (1)

Cody Gray
Cody Gray

Reputation: 244722

Defining an opaque handle to your entity and a bunch of function that take that handle as the first argument beautifully satisfies all requirements, but it is not idiomatic C++.

You can still encapsulate this in a class. The opaque handle would be the sole private data member of the class, its implementation not publically exposed in any way. Implementation-wise, it would just be a pointer to a private data structure, dereferenced by the member functions of the class. This is still a minor improvement over the C solution because all of the related data and functions would be encapsulated in a single class and it makes it unnecessary for the client to keep track of a handle and pass it to every function.

Yes, I suppose dereferencing a pointer introduces some trivial amount of overhead, but the C solution would have the same problem.

No code duplication is required, and although it could arguably be considered a hack (or at least, inelegant C++ design), it certainly is no more of a hack than the same approach implemented in C. The only difference is C programmers have a lower threshold for what a "hack" is because their language has less ways of expressing a design.

A rough sketch of the design I'm thinking of (basically the same as PIMPL, but with only the data members made opaque):

// In a header file:

class DrawingPen
{
  public:

     DrawingPen(...);   // ctor
     ~DrawingPen();     // dtor

     void SetThickness(int thickness);
     // ...and other member functions

  private:
     void *pPen;   // opaque handle to private data
};
// In an implementation file:

namespace {
struct DrawingPenData
{
    int thickness;
    int red;
    int green;
    int blue;
    // ... whatever else you need to describe the object or track its state
};
}


// Definitions of the ctor, dtor, member functions, etc.
// For instance:

void DrawingPen::SetThickness(int thickness)
{
    // Get the object data through the handle.
    DrawingPenData *pData = reinterpret_cast<DrawingPenData*>(this->pPen);

    // Update the thickness.
    pData->thickness = thickness;
}

If you need private functions that work on a DrawingPen, but that you do not want to expose in the DrawingPen header, you would just place them in the same anonymous namespace in the implementation file, accepting a reference to the class object.

Upvotes: 2

Related Questions