I am creating a shared library in Qt5 C++. To allow future updates preserving the binary compatibility I would like to use the d-pointer technique. However, I don't know how to apply it when there is a composition of classes. The examples I've found, including the one here, explain only the case of class inheritance. My question is
Do I need to make a corresponding private class for each class in the library (myLib, B and C) or only for the main one (myLib) and how to access them later?
Here is my setup and the desired functionality without the private classes:
myLib.h
#include "B.h"
class myLib;
{
public:
myLib();
B *getB(int n);
private:
QList<B *> m_b;
}
B.h
#include "C.h"
class B;
{
public:
B();
C *getC(int n);
private:
QList<C *> m_c;
}
C.h
class C;
{
public:
C();
int getVar();
private:
int m_var;
}
And somewhere in the main app:
myLib *m_lib = new myLib();
int k = m_lib->getB(4)->getC(2)->getVar();
From the linked: The problem "Never change the size of an exported C++ class".
The solution: "The trick is to keep the size of all public classes of a library constant by only storing a single pointer. This pointer points to a private/internal data structure that contains all the data.".
As long as your class is not shown to the consumers of your lib, feel free to go D-pointerless. By "shown to the consumers" I mean "with their full definition made available by declarations in the headers meant to be included by the consumer code". Perhaps the terms public/private are suffering from a 'semantic overload' here, let's use the 'exposed'/'opaque' (see the ** footnote)
In your example, both B
and C
are exposed, so they must be available "by pointers only".
The same goes about myLib
class. What is worse: instances of myLib
can be obtained by value, because the constructor is public
. This means I can do something like:
myLib libObj;
libObj.getB(4)->getC(2)->getVar();
which will make impossible to have "drop in replacements, no recompilation needed" from the future releases of myLib
.
I suggest forcing the consumers go through factory method to obtain instances of myLib
(or with a 'singleton'). Something on the line of:
class myLib {
private:
myLib() {
}
public:
static myLib* createInstance() {
return new myLib();
}
};
** As an example "exposed/opaque declaration" - the class B
is expose to the library consumer (which will know B
-s will have.. ahem... private parts), but about class M
the consumer knows only that it exists and the library will provide pointers to it:
file "myLib.hpp"
// M_type is a pointer to a class and that's all you,
// the consumer, need to know about it. You give me an M_type
// and ask specific questions about it and you'll
// get the details or access to data I choose to
// make available to you
typedef class M * M_type;
// Dear the library consumer, this class is public to you.
// You know I'm keeping a list of M_type, even if you also know
// you'll never get you hands directly on that list, because
// it has a private access. But having this information,
// **you can compute the sizeof(B)**.
class B {
public:
B();
M_type getM(int n);
const M_type getM(int n) const;
// that is one of the "questions" you can ask about an M_type
const char* getMLabel(const M_type var) const;
// I'm providing you with access to something that allows
// you to modify the amount stored by an M_type,
// even if you don't know (and never will) how
// I'm storing that amount
int& getMAmount(M_type var);
// You don't need to know how to create M-s, I'll
// be doing it for you and provide the index of the created
// M_type. Ask me with getM to get that pointer.
inr registerM(const char* label, int amount);
private:
QList<M_type> ems;
};
Somewhere, deep inside the library code, there will exist a header that defines what class M
is, and myLib.cpp
will include it, but that header will be used only to compile the library and never provided with myLib binary releases.
As such, class M
is opaque (as opposed to exposed) for the library consumer.