Search code examples
c++qtqt5compositiond-pointer

D-pointer and composition classes in a shared library


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();

Solution

  • 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.