Cyclic包含隐藏C ++头文件中的实现细节的技巧

时间:2011-10-08 15:19:28

标签: c++ include implementation

我正在尝试找到一种简洁的方法来分离大型项目中C ++头文件中的实现细节,以实现更好的信息隐藏并缩短构建时间。 C ++的问题在于,每次更改私有成员声明时,都必须重建依赖类。

这是我提出的解决方案。这有什么好处吗?

基本思路是在标题中有条件地包含 cpp 文件的一部分。此部分包含实现声明,仅在实现文件包含标头时包含。在外部类的情况下,此详细信息将从标题中排除。所以客户端和实现看到两个不同版本的头文件。内部声明更改不会影响客户端(不编译依赖类),标头也不会包含私有详细信息。

以下是实施:

标题

#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

客户端

#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 ?!!

}

4 个答案:

答案 0 :(得分:5)

真的,这是一个非常有趣的问题。管理依赖关系对于大型项目非常重要,因为构建时间的增加甚至可以使最简单的更改变得令人生畏......当它发生时,人们会尝试破解它以避免重建(tm)。

不幸的是,它不起作用。

标准明确指出,出现在不同翻译单元(粗略地说,文件)中的类定义应遵循一个定义规则(参见§3.2一个定义规则[basic.def.odr] )。

为什么?

在某种程度上,问题是阻抗问题。类的定义包含有关类ABI(应用程序二进制接口)的信息,最值得注意的是,这样的类是如何在内存中布局的。如果您在各种翻译单元中具有相同类的不同布局,那么在完全放置它时,它将无法工作。就像一个TU说德语和另一个韩语。他们可能试图说同样的话,他们只是不会互相理解。

那么?

有几种方法可以管理依赖项。主要的想法是,你应该尽可能地努力提供“轻”标题:

  • 包含尽可能少的东西。您可以转发声明:显示为参数的类型或返回函数声明,通过引用或指针传递但未使用的类型。
  • 隐藏实施细节
嗯......这是什么意思:x?

我们选择一个简单的例子,是吗?

#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

此标题包含5个其他标题,但实际需要多少个?

  • a.hpp是必要的,因为类型_a的{​​{1}}是该类的属性
  • A不是必需的,我们只提及b.hpp
  • B不是必需的,我们只有指向c.hpp
  • 的指针
  • C是必要的,我们在d.hpp
  • 上调用方法
  • D不是必需的,它只显示为返回

好的,让我们清理一下!

e.hpp

我们可以做得更好吗?

好吧,首先我们可以看到我们只在类的构造函数中调用#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 上的方法,如果我们将D的定义移出标题,并将其放在{D中1}}文件,然后我们将不再需要包含.cpp

d.hpp

但...... // no need to illustrate right now ;) 的内容是什么?

可以通过重写仅仅持有指针而不需要完整定义来“欺骗”。这被称为实现指针(简称pimpl)。它会减少运行时间以获得更轻的依赖性,并为类增加了一些复杂性。这是一个演示:

A

相应的源文件,因为那是有趣的事情发生:

#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

好的,这就是低调和坚韧不拔。进一步阅读:

  • Law of Demeter:避免必须在序列中调用多个方法(#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() { /* ... */ } ),这意味着你有漏洞抽象,并强迫你包括整个世界做任何事情。相反,您应该拨打a.b().c().d()来隐藏您的详细信息。
  • 将您的代码分成模块,并为每个模块提供一个明确定义的接口,通常,您应该在模块中拥有的代码远远多于其表面上的代码(即暴露的标题)。

有很多方法可以封装和隐藏信息,你的任务才刚刚开始!

答案 1 :(得分:4)

这不起作用。如果您在私有.cpp文件中向该类添加任何内容,则该类的用户将看到与您的实现认为不同的类。

这不合法,在许多情况下不起作用。 KDE有一篇很棒的文章,介绍了C ++中可以和不可以改变的内容,以保持ABI兼容性:Binary Compatibility Issues。如果您使用“隐藏”实现打破其中任何一项,那么您将打破用户。

查看pimpl idiom是否有一种相当常见的方式来实现您想要实现的目标。

答案 2 :(得分:2)

查看Opaque_pointer pattern(又名pImpl)

该模式通常用于Class希望隐藏内部实现的情况,但也有利于对内部和私有结构的更改不会创建重新编译,因为维护了二进制调用兼容性。

以任何其他方式执行此操作的问题是,当您更改类定义中的任何内容时,可能无法维护二进制兼容性,因此必须重新编译所有软件。

看起来您的解决方案是尝试这样做,但是您应该使用(void *)而不是int,以确保软件在不同平台上的32位和64位编译器上正确编译 - 以及只需使用不透明指针的烹饪书例子。

答案 3 :(得分:2)

这不起作用。您可以很容易地看到它,因为实现sizeof(Dependency)和客户端不同。客户端基本上看到一个不同的类,访问内存中的不同位置,一切都搞砸了!

不幸的是,如果更改类,则无法阻止重建依赖文件。但是,您可以隐藏实现细节,如下所示:

标题

class privateData;

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

cpp文件

#include <Dependency.h>

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

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

请注意,这不适合您复制粘贴。这只是为了给你这个想法。可能缺少此错误检查或此用法隐含的代码。其中一个是复制构造函数,以防止浅拷贝。