Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cyclic include trick to hide implementation details in C++ header files

I'm trying to find a clean way to separate implementation details in C++ header files in a big project in order to achieve better information hiding and reduce build time. The problem with C++ is that every time you change a private member declaration, your dependent classes must be rebuilt.

This is a solution I came up with. Is it any good?

The basic Idea is to include a part of the cpp file conditionally in the header. this part contains the implementation declarations and is included only when the implementation file includes the header. in case of the external classes, this details are excluded from header. so client and implementation see two different version of header file. internal declaration changes won't affect clients(no compilation of dependent classes) and headers won't include private details.

Here is the implementation:

HEADER

#pragma once

class Dependency
{
public:
    Dependency(void);
    ~Dependency(void);
    void Proc(void);

//PRIVATE Implementaion details stays private
#ifdef Dependency_PRIVATE_IMPELEMENTATION
    #define Dependency_PRIVATE_MODE 1   
        #include "Dependency.cpp"
    #undef Dependency_PRIVATE_MODE
#endif 
};

CPP

#define Dependency_PRIVATE_IMPELEMENTATION
#include "Dependency.h"
#undef Dependency_PRIVATE_IMPELEMENTATION

#ifdef Dependency_PRIVATE_MODE
private:
    int _privateData;
#else

#include <iostream>

Dependency::Dependency(void)
{
//This line causes a runtime exception, see client
    Dependency::_privateData = 0;
}

Dependency::~Dependency(void)
{
}

void Dependency::Proc(void)
{
    std::cout << "Shiny happy functions.";
}

#endif

CLIENT

#include "stdafx.h"
#include "Dependency.h"

#pragma message("Test.Cpp Compiled")

int _tmain(int argc, _TCHAR* argv[])
{
    Dependency d;
    d.Proc();

    return 0;
//and how I have a run time check error #2, stack around d ?!!

}
like image 601
Mehran Avatar asked Dec 06 '22 18:12

Mehran


2 Answers

It's a pretty interesting question, really. Managing dependencies is important for big projects because the build times ramp up can make even the simplest change daunting... and when it happens people will try to hack it to avoid the rebuild-of-death (tm).

Unfortunately, it does not work.

The Standard explicitly says that classes definitions appearing in different translation units (roughly, files) should obey the One Definition Rule (see § 3.2 One definition rule [basic.def.odr]).

Why ?

The problem is a matter of impedance, in a way. The definition of a class contains information on the class ABI (Application Binary Interface), most notably, how such a class is layed out in memory. If you have different layouts of the same class in various translation units, then when putting it altogether, it won't work. It's as if one TU was speaking German and the other Korean. They might be attempting to say the same thing, they just won't understand each other.

So ?

There are several ways to manage dependencies. The main idea is that you should struggle, as much as possible, to provide "light" headers:

  • include as few things as possible. You can forward declare: types that are shown as arguments or return of functions declaration, types that are passed by reference or pointer but otherwise unused.
  • hide implementation details

Hum... What does it mean :x ?

Let's pick a simple example, shall we ?

#include "project/a.hpp" // defines class A
#include "project/b.hpp" // defines class B
#include "project/c.hpp" // defines class C
#include "project/d.hpp" // defines class D
#include "project/e.hpp" // defines class E

namespace project {

  class MyClass {
  public:
    explicit MyClass(D const& d): _a(d.a()), _b(d.b()), _c(d.c()) {}
    MyClass(A a, B& b, C* c): _a(a), _b(b), _c(c) {}

    E e() const;

  private:
    A _a;
    B& _b;
    C* _c;
  }; // class MyClass

} // namespace project

This header pulls in 5 other headers, but how many are actually necessary ?

  • a.hpp is necessary, because _a of type A is an attribute of the class
  • b.hpp is not necessary, we only have a reference to B
  • c.hpp is not necessary, we only have a pointer to C
  • d.hpp is necessary, we call methods on D
  • e.hpp is not necessary, it only appears as a return

OK, let's clean this up!

#include "project/a.hpp" // defines class A
#include "project/d.hpp" // defines class D

namespace project { class B; }
namespace project { class C; }
namespace project { class E; }

namespace project {

  class MyClass {
  public:
    explicit MyClass(D const& d): _a(d.a()), _b(d.b()), _c(d.c()) {}
    MyClass(A a, B& b, C* c): _a(a), _b(b), _c(c) {}

    E e() const;

  private:
    A _a;
    B& _b;
    C* _c;
  }; // class MyClass

} // namespace project

Can we do better ?

Well, first we can see that we call methods on D only in the constructor of the class, if we move the definition of D out of the header, and put it in a .cpp file, then we won't need to include d.hpp any longer!

// no need to illustrate right now ;)

But... what of A ?

It is possible to "cheat", by remarking that merely holding a pointer does not requires a full definition. This is known as the Pointer To Implementation idiom (pimpl for short). It trades off run time for lighter dependencies, and adds some complexity to the class. Here is a demo:

#include <memory> // don't really worry about std headers,
                  // they are pulled in at one time or another anyway

namespace project { class A; }
namespace project { class B; }
namespace project { class C; }
namespace project { class D; }
namespace project { class E; }

namespace project {

  class MyClass {
  public:
    explicit MyClass(D const& d);
    MyClass(A a, B& b, C* c);
    ~MyClass(); // required to be in the source file now
                // because for deleting Impl,
                // the std::unique_ptr needs its definition

    E e() const;

  private:
    struct Impl;
    std::unique_ptr<Impl> _impl;
  }; // class MyClass

} // namespace project

And the corresponding source file, since that were the interesting things occur:

#include "project/myClass.hpp" // good practice to have the header included first
                               // as it asserts the header is free-standing

#include "project/a.hpp"
#include "project/b.hpp"
#include "project/c.hpp"
#include "project/d.hpp"
#include "project/e.hpp"

struct MyClass::Impl {
  Impl(A a, B& b, C* c): _a(a), _b(b), _c(c) {}

  A _a;
  B& _b;
  C* _c;
};

MyClass::MyClass(D const& d): _impl(new Impl(d.a(), d.b(), d.c())) {}
MyClass::MyClass(A a, B& b, C* c): _impl(new Impl(a, b, c)) {}
MyClass::~MyClass() {} // nothing to do here, it'll be automatic

E MyClass::e() { /* ... */ }

Okay, so that was the low and gritty. Further reading:

  • The Law of Demeter: avoid having to call multiple methods in sequences (a.b().c().d()), it means you have leaky abstraction, and forces you the include the whole world to do anything. Instead, you should be calling a.bcd() which hides the details from you.
  • Separate your code into modules, and provide a clear-well defined interface to each module, normally, you should have far more code within the module than on its surface (ie exposed headers).

There are many ways to encapsulate and hide information, your quest just begins!

like image 125
Matthieu M. Avatar answered Jan 04 '23 14:01

Matthieu M.


This does not work. If you add anything to the class in the private .cpp file, the the users of the class will see a different class than what your implementation thinks it is.

This is not legal, and won't work in many cases. KDE has a great article on what you can and can't change in C++ to preserve ABI compatibility: Binary Compatibility Issues. If you break any of that with your "hidden" implementation, you're going to break the users.

Look at the pimpl idiom for a rather common way of doing what you are trying to achieve.

like image 31
Mat Avatar answered Jan 04 '23 13:01

Mat