开发者

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++ he开发者_如何转开发ader 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 ?!!

}


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!


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.


This won't work. You can easily see it because sizeof(Dependency) for the implementation and the client are different. The client basically sees a different class, accesses different locations in the memory and everything messes up!

Unfortunately, you cannot prevent a rebuild of dependent files if you change a class. However, you can hide the implementation details like this:

Header:

class privateData;

class Dependency
{
private:
    privateData *pd;
public:
    Dependency(void);
    ~Dependency(void);
    void Proc(void);
};

cpp file

#include <Dependency.h>

class privateData
{
    /* your data here */
};

Dependency::Dependency()
{
    pd = new privateData;
}
Dependency::~Dependency()
{
    if (pd)
        delete pd;
}
void Dependency::Proc()
{
    /* your code */
}

Note that this is not for you to copy paste. It's just to give you the idea. There may be missing error checking or code that is implied by this usage. One such thing is a copy constructor to prevent shallow copies.


Look at the Opaque_pointer pattern (aka pImpl)

The pattern is typically used for when the Class want to hide the internal implementation but is also have the benefit of that changes to the internal and private structures does not create a recompile since the binary call compatibility is maintained.

The problem with doing it any other way, is that binary compatibility is probably NOT maintained when you changed anything in the class definition, and hence all software will have to be recompiled.

It looks like your solution is a attempt to do exactly this, however you should user a (void*) instead of a int so as to make sure the software compiles correctly on 32 and 64 bit compilers on different platforms -- and just use the cook-book example of Opaque Pointers.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜