Как лучше всего построить варианты одного и того же приложения на C / C ++? - PullRequest
6 голосов
/ 04 ноября 2008

У меня есть три тесно связанных приложения, которые построены из одного и того же исходного кода - скажем, 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 ]

12 голосов
/ 04 ноября 2008

Вам не всегда нужно принудительно устанавливать отношения наследования в приложениях, использующих общую кодовую базу. На самом деле.

Существует старый трюк 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

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

7 голосов
/ 04 ноября 2008

То, что вы пытаетесь сделать, очень похоже на «Линии продуктов». У Университета Карниги Мелон есть отличная страница по этой схеме: http://www.sei.cmu.edu/productlines/

Это в основном способ создания разных версий одного программного обеспечения с разными возможностями. Если вы представляете что-то вроде Quicken Home / Pro / Business, то вы на правильном пути.

Хотя это может быть не совсем то, что вы пытаетесь, методы должны быть полезны.

2 голосов
/ 05 ноября 2008

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

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

Поначалу это может показаться вам проблемой. В долгосрочной перспективе вы должны иметь более надежный и проверяемый процесс строительства и обслуживания. Вы также должны уметь лучше тестировать, не беспокоясь о всех вариациях #if.

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

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

Это та же самая игра, используете ли вы классы C ++ или работаете в основном на уровне общего языка C / C ++.

1 голос
/ 05 ноября 2008

Однако сегодня утром я выстрелил себе в ногу и потратил много времени, просто пропустив строку #include "app_defines.h" из одного файла. Все скомпилировалось нормально, но приложение зависало с AV при запуске.

Существует простое решение этой проблемы: включите предупреждения, чтобы, если APP_B не определен, ваш проект не компилировался (или по крайней мере выдавал достаточно предупреждений, чтобы вы знали, что что-то не так).

1 голос
/ 04 ноября 2008

Сделайте что-то вроде этого:

<code>
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();
1 голос
/ 04 ноября 2008

Проблема в том, что использование директивы #if с неопределенным именем действует так, как если бы оно было определено как 0. Этого можно избежать, если всегда сначала делать #ifdef, но это громоздко и чревато ошибками.

Несколько лучше использовать псевдонимы пространства имен и пространства имен.

* 1005 Е.Г. *

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
}

Или любая комбинация вышеперечисленного.

Преимущество заключается в том, что если вы забудете заголовок, вы получите неизвестные ошибки пространства имен от вашего компилятора i.s.o. из-за молчаливого сбоя, потому что для ПРИЛОЖЕНИЯ по умолчанию установлено значение 0.

При этом у меня была похожая ситуация, я решил реорганизовать все во многие библиотеки, из которых подавляющее большинство составлял общий код, и позволить системе контроля версий обрабатывать то, что происходит в другом приложении i.s.o. опираясь на определения и т. д. в коде.

В моем варианте это работает немного лучше, но я знаю, что это зависит от конкретного приложения, YMMV.

1 голос
/ 04 ноября 2008

Вы также можете найти некоторую помощь в этом, который я спросил: Написание кроссплатформенных приложений на C

1 голос
/ 04 ноября 2008

Если вы используете C ++, не должны ли ваши приложения A, B и C наследоваться от общего предка? Это был бы ОО-способ решения проблемы.

0 голосов
/ 30 апреля 2009

Проверьте Современный дизайн C ++ Александреску . Он представляет разработку на основе политики с использованием шаблонов. По сути, этот подход является расширением шаблона стратегии, с той лишь разницей, что все выборы делаются во время компиляции. Я думаю, что подход Александреску похож на использование языка PIMPL, но реализуется с помощью шаблонов.

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

0 голосов
/ 30 апреля 2009

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

Вместо -

#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

(возможно, что-то сделать с ПРИЛОЖЕНИЕМ в том же духе).

Попытка использовать неопределенный препроцессор функция выдаст предупреждение или ошибку большинством компиляторов (тогда как неопределенный препроцессор define просто автоматически принимает значение 0). Если заголовок не включен, вы сразу заметите это, особенно если вы «воспринимаете предупреждения как ошибки».

...