views:

373

answers:

11

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.

+1  A: 

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.

JesperE
The added functionality of B and C is quite 'deep' within the system, so that's not a trivial change. I think you're right that an OO approach may help (in fact I was refactoring it towards that when I blew my clean foot off)
Roddy
+1  A: 

You may also find some help in this similar one I asked: Writing cross-platform apps in C

Dinah
+1  A: 

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.

Pieter
+5  A: 

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.

Jere.Jones
> Quicken Home/Pro/Business - That's exactly it.
Roddy
A: 

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.

fhe
+1  A: 
eli
+1  A: 

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.

plinth
That's a little better but would not have caught my problem. I had APP_CONFIG defined, but forgot to include the "common include file" you show above.
Roddy
+1  A: 

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).

James Antill
That would work if my compiler (C++Builder) had that Warning as an option. It doesn't...
Roddy
+1  A: 

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.

orcmid
A: 

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".

Hexagon
A: 

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.

ceretullis