Как лучше всего создавать варианты одного и того же приложения 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" из одного файла. Все скомпилировалось нормально, но приложение вылетало с АВ при запуске.

Я хотел бы знать, как лучше всего справиться с этим. Раньше это обычно было одним из немногих случаев, когда я считал, что #define можно использовать (во всяком случае, в C++), но я все еще сильно ошибался, и компилятор не защитил меня.


person Roddy    schedule 04.11.2008    source источник


Ответы (10)


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

Есть старый трюк UNIX, когда вы настраиваете поведение своего приложения на основе argv[0], то есть имени приложения. Если я правильно помню (а прошло 20 лет с тех пор, как я смотрел на это), rsh и rlogin являются/были одной и той же командой. Вы просто выполняете настройку во время выполнения на основе значения argv[0].

Если вы хотите придерживаться конфигурации сборки, обычно используется этот шаблон. Ваша система сборки/makefile определяет символ команды, такой как 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

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

person plinth    schedule 04.11.2008
comment
Это немного лучше, но не уловило бы мою проблему. Я определил APP_CONFIG, но забыл включить общий включаемый файл, показанный выше. - person Roddy; 04.11.2008

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

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

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

person Jere.Jones    schedule 04.11.2008
comment
› Quicken Home/Pro/Business — это точно. - person Roddy; 04.11.2008

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

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

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

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

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

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

person orcmid    schedule 04.11.2008

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

person JesperE    schedule 04.11.2008
comment
Добавленная функциональность B и C довольно «глубока» в системе, так что это не тривиальное изменение. Я думаю, вы правы в том, что ОО-подход может помочь (на самом деле, я рефакторил его в этом направлении, когда сдул свою чистую ногу) - person Roddy; 04.11.2008

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

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

E.g.

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. молчаливого сбоя, потому что APPLICATION по умолчанию равен 0.

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

По моему мнению, это работает немного лучше, но я знаю, что это очень специфично для приложения, YMMV.

person Pieter    schedule 04.11.2008

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


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();
person eli    schedule 04.11.2008

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

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

person James Antill    schedule 04.11.2008
comment
Это сработало бы, если бы мой компилятор (C++Builder) имел это предупреждение в качестве опции. Это не... - person Roddy; 05.11.2008

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

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

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

person fhe    schedule 04.11.2008

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

Вместо -

#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). Если заголовок не включен, вы сразу это заметите, особенно если вы «рассматриваете предупреждения как ошибки».

person Hexagon    schedule 30.04.2009

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

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

person paxos1977    schedule 30.04.2009