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

Meh*_*ran 3 c++ implementation include

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

这是我提出的解决方案.这有什么好处吗?我不是C++专家,所以我很有可能完全错过了一些东西,所以请光临我.

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

这是实施:

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 
};
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

客户

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

}
Run Code Online (Sandbox Code Playgroud)

Mat*_* M. 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
Run Code Online (Sandbox Code Playgroud)

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

  • a.hpp是必要的,因为_atype 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
Run Code Online (Sandbox Code Playgroud)

我们可以做得更好吗?

好吧,首先我们可以看到我们D只在类的构造函数中调用方法,如果我们移出D标题的定义,并将其放在一个.cpp文件中,那么我们就不需要再包括了d.hpp!

// no need to illustrate right now ;)
Run Code Online (Sandbox Code Playgroud)

但是......那是A什么?

通过注意仅仅握住指针不需要完整的定义,可以"作弊".这被称为实现指针(简称pimpl).它会减少运行时间以获得更轻的依赖性,并为类增加了一些复杂性.这是一个演示:

#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
Run Code Online (Sandbox Code Playgroud)

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

#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() { /* ... */ }
Run Code Online (Sandbox Code Playgroud)

好吧,那就是低调和坚韧不拔.进一步阅读:

  • 得墨忒耳定律:避免了不得不调用序列(多种方法a.b().c().d()),这意味着你有漏水的抽象,并强迫你了包括全世界做任何事情.相反,你应该打电话a.bcd()来隐藏你的细节.
  • 将代码分离为模块,并为每个模块提供清晰明确的接口,通常,模块中的代码应该多于表面上的代码(即暴露的标头).

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