Какой лучший способ построить варианты одного и того же приложения C/C++
У меня есть три тесно связанных приложения, которые построены из одного и того же исходного кода - скажем, APP_A, APP_B и APP_C. APP_C - это расширенный набор APP_B, который, в свою очередь, является расширенным набором APP_A.
До сих пор я использовал определение препроцессора для определения создаваемого приложения, которое работало так.
// File: app_defines.h
#define APP_A 0
#define APP_B 1
#define APP_C 2
Мои параметры сборки IDE затем укажите (например)
#define APPLICATION APP_B
... и в исходном коде у меня будут такие вещи, как
#include "app_defines.h"
#if APPLICATION >= APP_B
// extra features for APPB and APP_C
#endif
Однако этим утром я выстрелил себе в ногу и потратил много времени, просто пропустив строку #include "app_defines.h" из одного файла. Все скомпилировалось нормально, но приложение зависало с AV при запуске.
Я хотел бы знать, каков будет лучший способ справиться с этим. Раньше, как правило, это один из немногих случаев, когда я думал, что можно использовать #define (в любом случае, в C++), но я все еще плохо себя чувствовал, и компилятор не защищал меня.
11 ответов
Вам не всегда нужно принудительно устанавливать отношения наследования в приложениях, которые используют общую базу кода. В самом деле.
Существует старый трюк UNIX, в котором вы настраиваете поведение вашего приложения на основе argv[0], то есть имени приложения. Если я правильно помню (прошло уже 20 лет с тех пор, как я на это посмотрел), rsh и rlogin - это одна и та же команда. Вы просто выполняете настройку во время выполнения, основываясь на значении argv[0].
Если вы хотите придерживаться конфигурации сборки, этот шаблон обычно используется. Ваша система сборки /make-файл определяет символ в команде, такой как, APP_CONFIG, чтобы быть ненулевым значением, тогда у вас есть общий включаемый файл с гайками и болтами конфигурации.
#define APP_A 1
#define APP_B 2
#ifndef APP_CONFIG
#error "APP_CONFIG needs to be set
#endif
#if APP_CONFIG == APP_A
#define APP_CONFIG_DEFINED
// other defines
#endif
#if APP_CONFIG == APP_B
#define APP_CONFIG_DEFINED
// other defines
#endif
#ifndef APP_CONFIG_DEFINED
#error "Undefined configuration"
#endif
Этот шаблон гарантирует, что конфигурация определена в командной строке и является действительной.
То, что вы пытаетесь сделать, кажется очень похожим на "Линии продуктов". У Университета Карниджи Мелон есть отличная страница по шаблону здесь: http://www.sei.cmu.edu/productlines/
Это в основном способ создания разных версий одного программного обеспечения с разными возможностями. Если вы представляете что-то вроде Quicken Home / Pro / Business, то вы на правильном пути.
Хотя это может быть не совсем то, что вы пытаетесь, методы должны быть полезны.
Мне кажется, что вы могли бы взглянуть на модульность вашего кода на отдельно скомпилированные элементы, построение вариантов из набора общих модулей и специфичного для варианта модуля верхнего (основного) модуля.
Затем укажите, какие из этих частей входят в сборку, какие заголовочные файлы используются при компиляции верхнего уровня и какие файлы.obj вы включаете в фазу компоновщика.
Вы можете найти это немного борьбы на первых порах. В долгосрочной перспективе вы должны иметь более надежный и проверяемый процесс строительства и обслуживания. Вы также должны уметь лучше тестировать, не беспокоясь о всех вариациях #if.
Я надеюсь, что ваше приложение пока еще не очень большое, и при раскрытии модульности его функций не придется иметь дело с большим комом грязи.
В какой-то момент вам могут потребоваться проверки во время выполнения, чтобы убедиться, что сборка использует согласованные компоненты для конфигурации приложения, которую вы намеревались, но об этом можно будет узнать позже. Вы также можете выполнить некоторую проверку согласованности во время компиляции, но большую часть этого вы получите с помощью заголовочных файлов и подписей точек входа в подчиненные модули, которые входят в определенную комбинацию.
Это та же самая игра, используете ли вы классы C++ или работаете в основном на уровне общего языка C/C++.
Вы также можете найти некоторую помощь в этом, который я спросил: Написание кросс-платформенных приложений на C
Проблема заключается в том, что использование директивы #if с неопределенным именем действует так, как если бы оно было определено как 0. Этого можно избежать, всегда сначала делая #ifdef, но это и громоздко, и подвержено ошибкам.
Несколько лучший способ - использовать псевдонимы пространства имен и пространства имен.
Например
namespace AppA {
// application A specific
}
namespace AppB {
// application B specific
}
И использовать ваш app_defines.h для создания псевдонимов пространства имен
#if compiler_option_for_appA
namespace Application = AppA;
#elif compiler_option_for_appB
namespace Application = AppB;
#endif
Или, если более сложные комбинации, вложенное пространство имен
namespace Application
{
#if compiler_option_for_appA
using namespace AppA;
#elif compiler_option_for_appB
using namespace AppB;
#endif
}
Или любая комбинация вышеперечисленного.
Преимущество состоит в том, что, если вы забудете заголовок, вы получите неизвестные ошибки пространства имен от вашего компилятора, так как он молча терпит неудачу, потому что для APPLICATION по умолчанию установлено значение 0.
Тем не менее, я был в похожей ситуации, я решил реорганизовать все во многие библиотеки, из которых подавляющее большинство было общим кодом, и позволить системе контроля версий обрабатывать то, что происходит в разных приложениях, полагаясь на определения и т. Д. в коде.
Это работает немного лучше в моем варианте, но я знаю, что это очень специфично для приложения, YMMV.
Однако этим утром я выстрелил себе в ногу и потратил много времени, просто пропустив строку #include "app_defines.h" из одного файла. Все скомпилировалось нормально, но приложение зависало с AV при запуске.
Существует простое решение этой проблемы: включите предупреждения, чтобы, если APP_B не был определен, ваш проект не компилировался (или, по крайней мере, выдавал достаточно предупреждений, чтобы вы знали, что что-то не так).
Сделайте что-то вроде этого:
CommonApp +----- AppExtender + = containment
^ ^ ^
| | | ^ = ineritance
AppA AppB AppC |
Поместите свой общий код в класс CommonApp и поместите вызовы интерфейса "AppExtender" в стратегических местах. Например, интерфейс AppExtender будет иметь такие функции, как afterStartup, afterConfigurationRead, beforeExit, getWindowTitle ...
Затем в основной части каждого приложения создайте правильный расширитель и передайте его в CommonApp:
--- main_a.cpp
CommonApp application;
AppA appA;
application.setExtender(&appA);
application.run();
--- main_a.cpp
CommonApp application;
AppB appB;
application.setExtender(&appB);
application.run();
Если вы используете C++, не должны ли ваши приложения A, B и C наследоваться от общего предка? Это был бы ОО-способ решения проблемы.
Возможно, вы захотите взглянуть на инструменты, которые поддерживают разработку продуктовых линеек и способствуют структурированному управлению вариантами.
Одним из этих инструментов являются pure:: варианты из http://www.pure-systems.com/, которые способны управлять изменчивостью посредством моделей функций и отслеживать различные места, в которых функция реализована в исходном коде.
Вы можете выбрать конкретное подмножество функций из модели объектов, проверяются ограничения между функциями и создается конкретный вариант вашей линейки продуктов, то есть конкретный набор файлов и определений исходного кода.
Чтобы решить конкретную техническую проблему незнания, когда определено определение препроцессора, существует простой, но эффективный прием.
Вместо -
#define APP_A 0
#define APP_B 1
#define APP_C 2
Используйте -
#define APP_A() 0
#define APP_B() 1
#define APP_C() 2
И в том месте, где запрашиваются версии, -
#if APPLICATION >= APP_B()
// extra features for APPB and APP_C
#endif
(возможно, что-то сделать с ПРИЛОЖЕНИЕМ в том же духе).
Попытка использовать неопределенную функцию препроцессора вызовет предупреждение или ошибку большинством компиляторов (тогда как неопределенное определение препроцессора просто молча оценивает 0). Если заголовок не включен, вы сразу заметите, особенно если вы "воспринимаете предупреждения как ошибки".
Проверьте Современный дизайн C++ Александреску. Он представляет разработку на основе политики с использованием шаблонов. По сути, этот подход является расширением шаблона стратегии, с той лишь разницей, что все выборы делаются во время компиляции. Я думаю, что подход Александреску похож на использование идиомы PIMPL, но реализуется с помощью шаблонов.
Вы должны использовать флаги предварительной обработки в общем заголовочном файле, чтобы выбрать, какую реализацию вы хотите скомпилировать, и указать ее для типа, используемого во всех экземплярах шаблона в другом месте вашей кодовой базы.