Это довольно интересный вопрос, правда.Управление зависимостями важно для больших проектов, потому что время сборки может сделать даже самое простое изменение пугающим ... и когда это произойдет, люди попытаются взломать его, чтобы избежать восстановления (tm).
К сожалению, это не работает.
Стандарт прямо заявляет, что определения классов, появляющиеся в разных единицах перевода (грубо говоря, в файлах), должны подчиняться Одному правилу определения (см. § 3.2.Одно определение правила [basic.def.odr] ).
Почему?
В некотором смысле, проблема заключается в импедансе.Определение класса содержит информацию о классе ABI (Application Binary Interface), в частности, о том, как такой класс размещается в памяти.Если у вас разные макеты одного и того же класса в разных единицах перевода, то при их совместном использовании это не сработает.Это как если бы один TU говорил по-немецки, а другой - по-корейски.Возможно, они пытаются сказать одно и то же, просто не понимают друг друга.
Итак?
Есть несколько способов управления зависимостями.Основная идея заключается в том, что вам нужно изо всех сил стараться обеспечить «легкие» заголовки:
- включает как можно меньше вещей.Вы можете пересылать объявление: типы, которые отображаются как аргументы или возвращают объявление функций, типы, которые передаются по ссылке или по указателю, но в противном случае не используются.
- скрыть детали реализации
Hum ..Что это значит: 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
isне обязательно, это только в виде возврата
Хорошо, давайте очистим это!
#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
из heaи поместите его в файл .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() { /* ... */ }
Хорошо, так что это был низкий и песчаный.Далее читаем:
- Закон Деметры : избегайте необходимости вызывать несколько методов в последовательностях (
a.b().c().d()
), это означает, что у вас есть дырявая абстракция, и вы заставляете включатьВесь мир, чтобы сделать что-нибудь.Вместо этого вы должны вызывать a.bcd()
, который скрывает от вас подробности. - Разделите ваш код на модули и предоставьте четко определенный интерфейс для каждого модуля, обычно у вас должно быть гораздо больше кода внутримодуля, чем на его поверхности (т.е. открытые заголовки).
Есть много способов инкапсулировать и скрыть информацию, ваш квест только начинается!