Perhaps I am trying to be too clever, but I think I cannot have been the first one to try this and come across this problem. And I am secretly hoping that one of you knows what to do or to link me to a similar question with some good answers.
I am trying to wrap a 3rd party pure C database API in C++ classes. To abstract the database API away at the application layer, I am putting all its types and functionality in a PIMPL layer. These classes will also double up as RAII types.
Application │ Database API
┌─────────────────────┐ │ ┌─────────┐
│ DatabaseConnection ├────────►│ Impl │
├─────────────────────┤ │ ├─────────┤
│ │ │ db_t │
└─────────────────────┘ │ └─────────┘
┌─────────────────────┐ │ ┌─────────────────┐
│ Schema ├────────►│ Impl │
├─────────────────────┤ │ ├─────────────────┤
│ │ │ db_tabledef_t │
└─────────────────────┘ │ └─────────────────┘
So to make it abundantly clear, db_t and db_tabledef_t are types from this database API.
The declaration of Schema will look something like this:
class Schema
{
class Impl;
friend class DatabaseConnection; // useful here at all?
public:
// ctors/dtor/others here
private:
std::unique_ptr<Impl> p_;
};
The DatabaseConnection class and others will look very similar. Meaning that the declaration of all "inner" PIMPL types will be inside the .cpp files.
That's my understanding on how the PIMPL idiom is implemented in C++, and one that std::unique_ptr seems to support.
At some point I need to bring these two or more bits of information together to make use of the database API.
As an example: at the first hurdle I wish to call a function on the database API to create the table in the database using a free function db_create_table(). For this I need both parts db_t and db_tabledef_t.
Obvious first inclination is to create method DatabaseConnection::createTable() taking a Schema as its parameter. This could of course delegate to its PIMPL type, which makes sense.
But how can I access Schema::Impl from with a function in DatabaseConnection or its PIMPL type?
It looks to me like you cannot get away with this design and not expose some of the database API at the application layer. Or in other words: one will need a "getter".
Or is there another - hopefully community endorsed - way that keeps the application layer interface clean from the C API types?
Side note I am tagging this question as C++ because that is what I am using, but other languages may face similar issues. If you know of one, drop a comment and I shall add that tag so we may widen the scope for future searches.
I was given this solution in another forum.
The "mistake" I made was that the private class was declared in the .cpp file. That means that it's perhaps a little too opaque. It can work that way, but only as a standalone PIMPL. As I found out when two PIMPLs need to interact, this becomes a design problem.
The suggestion is to move the declaration of the private class into a separate header. This header should not live among the interfaces but rather among the source files.
For example: the
Schema::Implclass declaration is moved wholesale into SchemaPrivate.h. Any definition for this class can still live in file Schema.cpp.You can then add any getter you need to these private classes. You make the classes at the interface level (in folder root/inc) friends where needed.
Now you could (example):
If you were to build this as a library using cmake you would perhaps:
So client code would inherit the public interface in root/Inc. But they would not implicitly get access to the private classes.
It's a compromise, but it's thus far the best solution under the circumstance.