Эффективная конфигурация иерархии классов во время компиляции

Этот вопрос специально посвящен архитектуре С++ для встроенных, жестких систем реального времени. Это означает, что во время компиляции приводятся большие части структур данных, а также точный программный поток, производительность важна, и много кода может быть встроено. Решения предпочтительно используют только С++ 03, но также приветствуются входы С++ 11.

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

Я, скорее всего, окажусь иерархической структурой модулей, инкапсулированных в классы, которые затем могут выглядеть примерно так, предполагая 4 слоя:

Product A                       Product B

Toplevel_A                      Toplevel_B                  (different for A and B, but with common parts)
    Middle_generic                  Middle_generic          (same for A and B)
        Sub_generic                     Sub_generic         (same for A and B)
            Hardware_A                      Hardware_B      (different for A and B)

Здесь некоторые классы наследуют от общего базового класса (например, Toplevel_A от Toplevel_base), в то время как другим не требуется специализироваться вообще (например, Middle_generic).

В настоящее время я могу думать о следующих подходах:

  • (A). Если это было обычное настольное приложение, я бы использовал виртуальное наследование и создавал экземпляры во время выполнения, используя, например, Аннотация Factory.

    Недостаток. Однако классы *_B никогда не будут использоваться в продукте A и, следовательно, разыменование всех вызовов виртуальных функций и членов, не связанных с адресом во время выполнения, приведет к довольно некоторые накладные расходы.

  • (B) Использование специализированной специализации в качестве механизма наследования (например, CRTP)

    template<class Derived>
    class Toplevel  { /* generic stuff ... */ };
    
    class Toplevel_A : public Toplevel<Toplevel_A> { /* specific stuff ... */ };
    

    Недостаток: трудно понять.

  • (C): используйте разные наборы совпадающих файлов и пусть скрипты build содержат правильный

    // common/toplevel_base.h
    class Toplevel_base { /* ... */ };
    
    // product_A/toplevel.h
    class Toplevel : Toplevel_base { /* ... */ };
    
    // product_B/toplevel.h
    class Toplevel : Toplevel_base { /* ... */ };
    
    // build_script.A
    compiler -Icommon -Iproduct_A
    

    Недостаток. Сложность, сложность в обслуживании и тестировании.

  • (D): один большой файл typedef (или #define)

    //typedef_A.h
    typedef Toplevel_A Toplevel_to_be_used;
    typedef Hardware_A Hardware_to_be_used;
    // etc.
    
    // sub_generic.h
    class sub_generic {
        Hardware_to_be_used the_hardware;
        // etc.
    };
    

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

  • (E): аналогичная, "политическая" настройка, например

    template <class Policy>
    class Toplevel { 
        Middle_generic<Policy> the_middle;
        // ...
    };
    
    // ...
    
    template <class Policy>
    class Sub_generic {
        class Policy::Hardware_to_be_used the_hardware;
        // ... 
    };
    
    // used as
    class Policy_A {
        typedef Hardware_A Hardware_to_be_used;
    };
    Toplevel<Policy_A> the_toplevel;
    

    Недостаток: теперь все шаблоны; много кода нужно перекомпилировать каждый раз.

  • (F): компилятор и препроцессор

    // sub_generic.h
    class Sub_generic {
        #if PRODUCT_IS_A
            Hardware_A _hardware;
        #endif
        #if PRODUCT_IS_B
            Hardware_B _hardware;
        #endif
    };
    

    Недостаток: Brrr..., только если все остальное не работает.

Есть ли какой-либо (другой) установленный шаблон проектирования или лучшее решение этой проблемы, так что компилятор может статически распределять как можно больше объектов и встроенных больших частей кода, зная, какой продукт строится и какие классы будут использоваться?

Ответ 1

Сначала я хотел бы отметить, что вы в основном ответили на свой вопрос в вопросе: -)

Далее я хотел бы указать, что в С++

точный программный поток задается во время компиляции, производительность важно и много кода может быть встроено

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

Кроме того, как отмечено в других ответах, C более распространен для жестких систем реального времени, чем С++, а в C обычно полагаться на MACROS, чтобы сделать такую ​​оптимизацию во время компиляции.

Наконец, вы отметили в своем решении B выше, что специализация шаблона трудно понять. Я бы сказал, что это зависит от того, как вы это делаете, а также от того, сколько опыта у вашей команды на С++/templates. Я считаю, что многие проекты с "шаблонами" чрезвычайно трудны для чтения, а сообщения об ошибках, которые они производят, в лучшем случае являются нечестивыми, но мне все же удается эффективно использовать шаблоны в моих собственных проектах, потому что я уважаю принцип KISS во время его выполнения.

Итак, мой ответ на ваш вопрос: перейдите с B или выберите С++ для C

Ответ 2

Я бы пошел на A. До тех пор, пока он НЕ ПРЕДОСТАВЛЯЕТ, что это недостаточно, пойти на те же решения, что и на рабочем столе (ну, конечно, выделяя несколько килобайт в стеке или используя глобальные переменные, которые имеют много мегабайт больших может быть "очевидным", что он не будет работать). Да, есть некоторая накладная часть при вызове виртуальных функций, но я бы пошел на самое очевидное и естественное решение С++ FIRST, а затем перепроектировал, если он не "достаточно хорош" (очевидно, попытайтесь определить производительность и так рано, и используйте такие инструменты, как профайлер пробоотбора, чтобы определить, где вы проводите время, а не "угадывание" - люди оказались довольно безнадзорными).

Затем я перейду к опции B, если доказано, что A не работает. Это действительно не совсем очевидно, но, грубо говоря, как LLVM/Clang решает эту проблему для комбинаций аппаратного обеспечения и ОС, см. https://github.com/llvm-mirror/clang/blob/master/lib/Basic/Targets.cpp

Ответ 3

Я понимаю, что у вас есть два важных требования:

  • Типы данных известны во время компиляции
  • Программный поток известен во время компиляции

CRTP не будет действительно решать проблему, которую вы пытаетесь решить, поскольку это позволит HardwareLayer вызывать методы на Sub_generic, Middle_generic или TopLevel, и я не считаю, что это то, что вы ищете.

Оба ваших требования могут быть выполнены с помощью Trait pattern (другой ссылка). Вот пример, подтверждающий выполнение обоих требований. Сначала мы определяем пустые оболочки, представляющие два жестких диска, которые вы можете поддержать.

class Hardware_A {};
class Hardware_B {};

Тогда рассмотрим класс, описывающий общий случай, соответствующий Hardware_A.

template <typename Hardware>
class HardwareLayer
{
public:
    typedef long int64_t;

    static int64_t getCPUSerialNumber() {return 0;}
};

Теперь рассмотрим специализацию для Hardware_B:

template <>
class HardwareLayer<Hardware_B>
{
public:
    typedef int int64_t;

    static int64_t getCPUSerialNumber() {return 1;}
};

Теперь вот пример использования в слое Sub_generic:

template <typename Hardware>
class Sub_generic
{
public:
    typedef HardwareLayer<Hardware> HwLayer;
    typedef typename HwLayer::int64_t int64_t;

    int64_t doSomething() {return HwLayer::getCPUSerialNumber();}
};

И, наконец, короткая основа, которая выполняет оба пути кода и использует оба типа данных:

int main(int argc, const char * argv[]) {
    std::cout << "Hardware_A : " << Sub_generic<Hardware_A>().doSomething() << std::endl;
    std::cout << "Hardware_B : " << Sub_generic<Hardware_B>().doSomething() << std::endl;
}

Теперь, если ваш HardwareLayer должен поддерживать состояние, вот еще один способ реализовать классы слоя HardLayer и Sub_generic.

template <typename Hardware>
class HardwareLayer
{
public:
    typedef long hwint64_t;

    hwint64_t getCPUSerialNumber() {return mySerial;}

private:
    hwint64_t mySerial = 0;
};

template <>
class HardwareLayer<Hardware_B>
{
public:
    typedef int hwint64_t;

    hwint64_t getCPUSerialNumber() {return mySerial;}

private:
    hwint64_t mySerial = 1;
};

template <typename Hardware>
class Sub_generic : public HardwareLayer<Hardware>
{
public:
    typedef HardwareLayer<Hardware> HwLayer;
    typedef typename HwLayer::hwint64_t hwint64_t;

    hwint64_t doSomething() {return HwLayer::getCPUSerialNumber();}
};

И вот последний вариант, где изменяется только реализация Sub_generic:

template <typename Hardware>
class Sub_generic
{
public:
    typedef HardwareLayer<Hardware> HwLayer;
    typedef typename HwLayer::hwint64_t hwint64_t;

    hwint64_t doSomething() {return hw.getCPUSerialNumber();}

private:
    HwLayer hw;
};

Ответ 4

Так как это для жесткой встроенной системы реального времени, обычно вы должны использовать для C-типа решение, а не С++.

С современными компиляторами я бы сказал, что накладные расходы на С++ не такие уж большие, так что это не совсем вопрос производительности, но встроенные системы предпочитают c вместо С++. То, что вы пытаетесь построить, будет напоминать классическую библиотеку драйверов устройств (например, для чипов ftdi).

Подход, который был бы (поскольку он написан на C) чем-то похожим на ваш F, но без параметров времени компиляции - вы бы специализировали код во время выполнения на основе somethig, такого как PID, VID, SN и т.д..

Теперь, если вы используете для этого С++, шаблоны, вероятно, должны быть вашим последним вариантом (читаемость кода обычно занимает больше, чем любые шаблоны преимуществ, приносимые в таблицу). Таким образом, вы, вероятно, захотите что-то похожее на A: базовую схему наследования классов, но не требуется особо причудливого шаблона проектирования.

Надеюсь, что это поможет...

Ответ 5

На подобном пути мысли к F вы можете просто создать макет каталога следующим образом:

Hardware/
  common/inc/hardware.h
  hardware1/src/hardware.cpp
  hardware2/src/hardware.cpp

Упростите интерфейс, чтобы предположить, что существует только одно оборудование:

// sub_generic.h
class Sub_generic {
        Hardware _hardware;
};

И тогда только скомпилируйте папку, содержащую файлы .cpp для аппаратного обеспечения для этой платформы.

Преимущества этого подхода заключаются в следующем:

  • Просто понять, что происходит и добавить hardware3
  • hardware.h по-прежнему служит вашим API
  • Отнимает абстракцию от компилятора (для вашей скорости)
  • Компилятор 1 не нуждается в компиляции hardware2.cpp или hardware3.cpp, который может содержать вещи, которые не может выполнить компилятор 1 (например, встроенная сборка или какая-либо другая конкретная вещь компилятора 2).
  • hardware3 может быть намного сложнее по какой-то причине, о котором вы еще не подумали... поэтому предоставление ему цельной структуры каталога инкапсулирует его.

Ответ 6

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

В этом случае я бы рекомендовал использовать шаблон Object Factory, так как Factory будет запускаться только один раз для создания класса. С этой точки на специализированных классах все известные типы.