Cyclic включает трюк, чтобы скрыть детали реализации в заголовочных файлах C ++ - PullRequest
3 голосов
/ 08 октября 2011

Я пытаюсь найти простой способ отделить детали реализации в заголовочных файлах 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 
};

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 ]

5 голосов
/ 08 октября 2011

Это довольно интересный вопрос, правда.Управление зависимостями важно для больших проектов, потому что время сборки может сделать даже самое простое изменение пугающим ... и когда это произойдет, люди попытаются взломать его, чтобы избежать восстановления (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(), который скрывает от вас подробности.
  • Разделите ваш код на модули и предоставьте четко определенный интерфейс для каждого модуля, обычно у вас должно быть гораздо больше кода внутримодуля, чем на его поверхности (т.е. открытые заголовки).

Есть много способов инкапсулировать и скрыть информацию, ваш квест только начинается!

4 голосов
/ 08 октября 2011

Это не работает.Если вы добавите что-либо к классу в личном файле .cpp, пользователи класса увидят класс, отличный от того, что думает ваша реализация.

Это недопустимо и не будет работатьво многих случаях.KDE имеет отличную статью о том, что вы можете и не можете изменить в C ++ для сохранения совместимости ABI: Проблемы двоичной совместимости .Если вы сломаете что-либо из этого с помощью своей «скрытой» реализации, вы сломаете пользователей.

Посмотрите на pimpl идиому для довольно распространенного способа делать то, что выпытаемся достичь.

2 голосов
/ 08 октября 2011

Это не сработает.Вы можете легко увидеть это, потому что 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 */
}

Обратите внимание, что это не для вас, чтобы скопировать вставить.Это просто чтобы дать вам идею.Возможно, отсутствует проверка ошибок или код, подразумеваемый этим использованием.Одной из таких вещей является конструктор копирования, предотвращающий мелкие копии.

2 голосов
/ 08 октября 2011

Посмотрите на шаблон Opaque_pointer (он же pImpl)

Шаблон обычно используется, когда класс хочет скрыть внутреннюю реализацию, но также имеет преимущество, заключающееся в том, что он изменяется навнутренние и частные структуры не создают перекомпиляцию, поскольку поддерживается совместимость двоичных вызовов.

Проблема с выполнением этого любым другим способом заключается в том, что двоичная совместимость, вероятно, НЕ поддерживается, когда вы что-либо изменили в определении класса, иследовательно, все программное обеспечение придется перекомпилировать.

Похоже, ваше решение является попыткой сделать именно это, однако вы должны использовать (void *) вместо int, чтобы убедиться, что программное обеспечение компилируется правильнона 32- и 64-битных компиляторах на разных платформах - и просто используйте пример поваренной книги Opaque Pointers.

...