Search code examples
c++conditional-compilation

Alternative to conditional compilation in c++, using multiple .cpp files


Suppose I have a C++ class that uses conditional compilation:

C.hpp

namespace NS{
  class C {
    public:
      C(void);
      ~C(void);
      int func( int arg1 );
    private:
      int memberVar;
}

C.cpp:

#include "C.hpp"

namespace NS{
  C::C( void ){ memberVar = 0; }
  C::~C( void ) {}
  int C::func( int arg1 ){
    int retval = 0;
    memberVar = arg1;
#ifdef DEV_BUILD
    retval = memberVar;
    printf( "memberVar was set.\n" );
#endif
    return retval;
  }
}

(this is a simplified example; imagine that the class C is several hundred lines long, with some of its functions using conditional compilation, each using the same condition: #ifdef DEV_BUILD. The application is an embedded-systems application, and the condition is used to distinguish between test hardware and production hardware, which have some differences)

I've been advised to instead implement the funcs in separate .cpp files... but I'm sort of hung up as to what the best way is.

My first impulse was to create C_dev.cpp and C_prod.cpp, implement C::func() differently in each, and edit C.cpp thusly:

#include "C.hpp"
#ifdef DEV_BUILD
#include "C_dev.cpp"
#else
#include "C_prod.cpp"
#endif

namespace NS{
  C::C( void ){ memberVar = 0; }
  C::~C( void ) {}
}

...but is this bad style? Or otherwise problematic?

Additional restrictions:

  • Can't use subclasses.
  • Can't use template classes.

(Update: Using separate files was suggested, but not required. I'm open to other suggestions)


Solution

  • Big point number 1: If you have hundreds of minor changes scattered all over the place in a sea of otherwise identical code, go with the conditional compilation. It will usually be easier to read and maintain.

    But if you have differences broken down at the function level, you can split the different functions among the different implementation files. Usually I'm doing this with operating system wrappers, and the differences in, say, getting a directory list on Linux and Windows are pretty coarse-grained. (Note: This example has been rendered obsolete by C++17)

    Big point number 2: Don't make multiple headers. This just compounds the problem. You want to expose a common interface between the two implementations. If you can't, you've already lost and need to find a different path. Make a common header that fits all of the implementations.

    For this answer all implementations use the Asker's original header, C.hpp:

    namespace NS{
      class C {
        public:
          C(void);
          ~C(void);
          int func( int arg1 );
        private:
          int memberVar;
    }
    

    I cut the Asker's example up into three cpp files: One with common functionality across all of the builds and two implementing the the functions containing differences. This way you aren't repeating any functions you don't absolutely need to repeat.

    C_common.cpp All of the shared functionality goes here

    #include "C.hpp"
    
    namespace NS{
      C::C( void ){ memberVar = 0; }
      C::~C( void ) {}
    }
    

    C_debug.cpp: functions with debug statements go here

    #include "C.hpp"
    
    namespace NS{
      int C::func( int arg1 ){
        int retval = 0;
        memberVar = arg1;
        retval = memberVar;
        printf( "memberVar was set.\n" );
        return retval;
      }
    }
    

    C_no_debug.cpp: functions without debug statements go here

    #include "C.hpp"
    
    namespace NS{
      int C::func( int arg1 ){
        memberVar = arg1;
        return memberVar;
      }
    }
    

    When you link the program, you always link C_common and specify which of C_debug and C_no_debug to link.

    You can sometimes take this a step further by breaking down complex functions and isolating only the differences and giving the differences a function called from within a shared function.

    C_common.cpp

    #include "C.hpp"
    
    namespace NS{
      C::C( void ){ memberVar = 0; }
      C::~C( void ) {}
      int C::func( int arg1 ){
        memberVar = arg1;
        debugstuff(); // calls the different functionality
        return memberVar;
      }
    }
    

    C_debug.cpp:

    #include "C.hpp"
    
    namespace NS{
        void debugstuff()
        {
            printf( "memberVar was set.\n" );
        }
    }
    

    C_no_debug.cpp:

    #include "C.hpp"
    
    namespace NS{
        void debugstuff()
        {
        }
    }
    

    This tends to scale poorly as you may wind up with many one liner functions. See the Big point number 1 above. But if the differences are shaped just right you can take advantage of passing function parameters to reduce the spam. In this case the logical thing to do is pass in the debug string. One prints and the other discards.

    C_common.cpp

    #include "C.hpp"
    
    namespace NS{
      C::C( void ){ memberVar = 0; }
      C::~C( void ) {}
      int C::func( int arg1 ){
        memberVar = arg1;
        debugstuff("memberVar was set.\n"); // calls the different functionality
                                            // and tells it what to do!
        return memberVar;
      }
    }
    

    C_debug.cpp:

    #include "C.hpp"
    
    namespace NS{
        void debugstuff(const char * message)
        {
            printf( message );
        }
    }
    

    C_no_debug.cpp:

    #include "C.hpp"
    
    namespace NS{
        void debugstuff(const char * )
        {
        }
    }