Roddy
Roddy

Reputation: 68023

What's the best way to build variants of the same C/C++ application

I have three closely related applications that are build from the same source code - let's say APP_A, APP_B, and APP_C. APP_C is a superset of APP_B which in turn is a superset of APP_A.

So far I've been using a preprocessor define to specify the application being built, which has worked like this.

// File: app_defines.h
#define APP_A 0
#define APP_B 1
#define APP_C 2

My IDE build options then specify (for example)

#define APPLICATION APP_B

... and in source code, I will have things like

#include "app_defines.h"

#if APPLICATION >= APP_B
// extra features for APPB and APP_C
#endif

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.

I'd like to know what a better way of handling this would be. Previously, This would normally one of the few times when I'd consider #define could be used (in C++, anyway), but I still goofed up badly and the compiler didn't protect me.

Upvotes: 6

Views: 2251

Answers (10)

eli
eli

Reputation: 672

Do something like this:

CommonApp   ├─────   AppExtender                       ├─ = containment
                      ▲    ▲    ▲
                      │    │    │                       ▲ = ineritance
                    AppA  AppB  AppC                    │

Put your common code in the class CommonApp and put calls to the interface 'AppExtender' at strategic places. For example the AppExtender interface will have functions like afterStartup, afterConfigurationRead, beforeExit, getWindowTitle ...

Then in the main of each application, create the correct extender and pass it to the 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();

Upvotes: 1

oz10
oz10

Reputation: 158264

Check out Alexandrescu's Modern C++ Design. He presents the policy based development using templates. Basically, this approach is an extension of the strategy pattern with the difference being that all choices are made at compile time. I think of Alexandrescu's approach as being similar to using the PIMPL idiom, but implementing with templates.

You would use the pre-processing flags in a common header file to choose which implementation that you wanted to compile, and typedef that to a type used in all the template instantiations elsewhere in your code-base.

Upvotes: 0

Hexagon
Hexagon

Reputation: 6961

To address the specific technical problem of not knowing when a preprocessor define is defined or not, there is a simple but effective trick.

Instead of -

#define APP_A 0
#define APP_B 1
#define APP_C 2

Use -

#define APP_A() 0
#define APP_B() 1
#define APP_C() 2

And in the place that queries for the version use -

#if APPLICATION >= APP_B()
// extra features for APPB and APP_C
#endif

(potentially do something with APPLICATION as well in the same spirit).

Trying to use an undefined preprocessor function would produce a warning or an error by most compilers (whereas an undefined preprocessor define simply evaluates to 0 silently). If the header isn't included, you would immediately notice - especially if you "treat warnings as errors".

Upvotes: 0

orcmid
orcmid

Reputation: 2638

It sounds to me that you might look at modularizing your code into separately-compiled elements, building the variants from a selection of common modules and a variant-specific top-level (main) module.

Then control which ones of these parts go into a build by which header files are used in compiling the top level and which .obj files you include into the linker phase.

You might find this a bit of a struggle at first. In the long run you should have a more reliable and verifiable construction and maintenance process. You should also be able to do better testing without worrying about all the #if variations.

I'm hoping that your application is not terribly large just yet and unraveling a modularization of its functions won't have to deal with a big ball of mud.

At some point you might need run-time checks to verify that the build used consistent components for the application configuration you intended, but that can be figured out later. You can also achieve some compile-time consistency checking, but you'll get most of that with header files and signatures of entry points into the subordinate modules that go into a particular combination.

This is the same game whether you are using C++ classes or operating pretty much at the C/C++ common-language level.

Upvotes: 2

James Antill
James Antill

Reputation: 2855

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.

There is a simple fix to this problem, turn on the warnings so that if APP_B isn't defined then your project doesn't compile (or at least produces enough warnings so that you know something is wrong).

Upvotes: 1

plinth
plinth

Reputation: 49179

You don't always have to force inheritance relationships in applications that share a common code base. Really.

There's an old UNIX trick where you tailor the behavior of you application based on argv[0], ie, the application name. If I recall correctly (and it's been 20 years since I looked at it), rsh and rlogin are/were the same command. You simply do runtime configuration based on the value of argv[0].

If you want to stick with build configuration, this is the pattern that is typically used. Your build system/makefile defines a symbol on the command like, APP_CONFIG to be a non-zero value then you have a common include file with the configuration nuts and bolts.

#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

This pattern enforces that the configuration is command line defined and is valid.

Upvotes: 12

fhe
fhe

Reputation: 6187

You might want to have a look at tools that support the development of product lines and foster explicit variant management in a structured way.

One of these tools is pure::variants from pure-systems which is capable of variability management through feature models and of keeping track of the various places a feature is implemented in source code.

You can select a specific subset of feature from the feature model, constraints between features are being checked, and the concrete variant of your product line, that is, a specific set of source code files and defines is created.

Upvotes: 0

Pieter
Pieter

Reputation: 17705

The problem is that using a #if directive with a name that's undefined acts as if it's defined as 0. This could be avoided by always doing an #ifdef first, but that's both cumbersome and error prone.

A slightly better way is to use namespace and namespace aliasing.

E.g.

namespace AppA {
     // application A specific
}

namespace AppB {
    // application B specific
}

And use you app_defines.h to do namespace aliasing

#if compiler_option_for_appA
     namespace Application = AppA;
#elif compiler_option_for_appB
     namespace Application = AppB;
#endif

Or, if more complex combinations, namespace nesting

namespace Application
{
  #if compiler_option_for_appA
     using namespace AppA;
  #elif compiler_option_for_appB
     using namespace AppB;
  #endif
}

Or any combination of the above.

The advantage is that when you forget the header you'll get unknown namespace errors from your compiler i.s.o. of silently failing because APPLICATION is defaulted to 0.

That being said, I've been in a similar situation, I chose to refactor everything into many libraries, of which the vast majority was shared code, and let the version control system handle what goes where in the different application i.s.o. relying on defines etc. in the code.

It works a bit better in my opionon, but I'm aware that happens to be very application specific, YMMV.

Upvotes: 1

Jere.Jones
Jere.Jones

Reputation: 10123

What you are trying to do seems very similar to "Product lines". Carnigie Melon University has an excellent page on the pattern here: http://www.sei.cmu.edu/productlines/

This is basically a way to build different versions of one piece of software with different capabilities. If you imagine something like Quicken Home/Pro/Business then you are on track.

While that may not be exactly what you attempting, the techniques should be helpful.

Upvotes: 7

JesperE
JesperE

Reputation: 64404

If you're using C++, shouldn't your A, B, and C applications inherit from a common ancestor? That would be the OO way to solve the problem.

Upvotes: 1

Related Questions