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

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
0
1 259
10

Ответы 10

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

Добавленная функциональность B и C довольно «глубокая» внутри системы, так что это нетривиальное изменение. Я думаю, вы правы в том, что объектно-ориентированный подход может помочь (на самом деле я рефакторинг его в сторону этого, когда я сорвал чистую ногу)

Roddy 04.11.2008 16:17

Проблема в том, что использование директивы #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
}

Или любое сочетание вышеперечисленного.

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

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

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

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

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

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

> Quicken Home / Pro / Business - вот именно.

Roddy 04.11.2008 17:35

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

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

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

Сделайте что-нибудь вроде этого:


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();

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

Есть старый трюк 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

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

Это немного лучше, но я бы не поймал мою проблему. У меня был определен APP_CONFIG, но я забыл включить «общий включаемый файл», который вы показываете выше.

Roddy 04.11.2008 17:34

However, I shot myself in the foot this morning and wasted far to much time by simply omitting the line to #include "app_defines.h" from one file. Everything compiled fine, but the application crashed with AVs at startup.

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

Это сработало бы, если бы мой компилятор (C++ Builder) имел это предупреждение в качестве опции. Это не ...

Roddy 05.11.2008 01:13

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

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

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

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

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

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

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

Вместо -

#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, но реализуется с помощью шаблонов.

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

Другие вопросы по теме