这是一个非常有趣的问题,真的。管理依赖项对于大型项目很重要,因为构建时间的增加甚至会使最简单的更改令人生畏……当它发生时,人们会尝试破解它以避免死亡重建 (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 类型的 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的定义移出头部,放到一个.cpp文件中,那么我们不再需要包含d.hpp!
// no need to illustrate right now ;)
但是...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
以及相应的源文件,因为发生了有趣的事情:
#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() { /* ... */ }
好的,这就是低调而坚韧的。延伸阅读:
-
Law of Demeter:避免顺序调用多个方法(
a.b().c().d()),这意味着您有泄漏的抽象,并迫使您包含整个世界来做任何事情。相反,您应该致电 a.bcd(),它会向您隐藏详细信息。
- 将您的代码分成多个模块,并为每个模块提供一个定义明确的接口,通常情况下,模块内的代码应该比其表面上的代码多得多(即暴露的标头)。
有很多方法可以封装和隐藏信息,你的探索才刚刚开始!