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