2

Due to resource limitations (embedded), I want to make some functionality that a class provides optional. The decision to include this optional functionality should be made at compile time.

For example, I have a simple pool-based memory allocator, which has optional statistics support. The statistics support should be available for x86 and ARM debug builds, but not for ARM release builds.

The following code is an excerpt from the current, C-based implementation, showing the kind of interaction between the pool itself and a conditionally included statistics member struct:

struct fmpool {
  // ...
  #ifdef FMPOOL_SUPPORT_STATISTICS
  fmpool_statistics stats;
  #endif
  // ...
}

int fmpool_alloc(fmpool *pool, void **alloc_block, size_t alloc_size)
{

  // Do the allocation

  #ifdef FMPOOL_SUPPORT_STATISTICS
  selected_pool->stats.blocks_used++;
  if (selected_pool->stats.blocks_used > selected_pool->stats.blocks_used_peak)
  {
    selected_pool->stats.blocks_used_peak = selected_pool->stats.blocks_used;
  }
  debug_log_pool_info(selected_pool);
  #endif

  // ...
}

I am not sure which pattern fits this use case best, but would like to unclutter the code of the allocator by removing the statistics code from the allocator itself.

In addition, the code is horribly cluttered with #ifdefs, so it is desireable to reduce the interaction between allocator code and allocator statistics code as much as possible.

3

Continuing to #ifdef is perfectly fine in C++. These kind of feature toggles are a good fit for the preprocessor.

Alternatively, you can extract the optional features into a separate struct/class and then either use compile-time templates or run-time polymorphism to select the correct implementation. Here, the closest equivalent would be to define a template. It goes a little something like this:

template<class Statistics>
struct fmpool {
  // ...
  Statistics stats {};
  // ...
};

template<class Statistics>
int fmpool_alloc(fmpool<Statistics>& pool, void **alloc_block, size_t alloc_size)
{

  // Do the allocation

  selected_pool.stats.event_allocation(selected_pool);

  // ...
}

Next, we define statistics types that can be selected. If statistics are disabled, we supply a null object that does nothing.

class StatisticsDisabled {
  template<class Pool>
  void event_allocation(Pool& pool) { /* nop */ }
};

struct StatisticsEnabled {
  unsigned blocks_used;
  unsigned blocks_used_peak;

  template<class Pool>
  void event_allocation(Pool& pool) {
    blocks_used++;
    if (blocks_used > blocks_used_peak)
    {
      blocks_used_peak = blocks_used;
    }
    debug_log_pool_info(pool);
  }
};

Finally, the fmpool can be instantiated with the correct statistics, possibly using the same #ifdef:

#ifdef FMPOOL_SUPPORT_STATISTICS
  using Pool = fmpool<StatisticsEnabled>;
#else
  using Pool = fmpool<StatisticsDisabled>;
#endif

Pool mypool;

What is the overhead of these templates?

  • As with the original macros there should be no run-time overhead. The compiler should be able to optimize the function calls to the statistics object away. Templated functions are implicitly inline, which means that the can be inlined. If you want to enforce that they will be inlined, you will need to use compiler-specific annotations.

  • There is however a compile-time overhead. Any code that uses your pool would have to be templated as well, and your pool implementation must be done in a header. As a consequence, compilation times noticeably suffer on non-small projects.

What are the alternatives?

  • Polymorphism has some run-time overhead but lets you avoid templates. Here we'd have to define an additional statistics interface struct Statistics { virtual void event_allocation(fmpool&) = 0; } that is inherited by the StatisticsEnabled and StatisticsDisabled types. The pool would then contain a std::unique_ptr<Statistics> stats member that may hold either type. The correct implementation can be provided in a constructor.

  • Here, you can keep the structure of the template solution but get rid of the templates. Instead, the correct member type is directly selected by conditional compilation:

    struct fmpool {
      // ...
      #ifdef FMPOOL_SUPPORT_STATISTICS
        StatisticsEnabled stats;
      #else
        StatisticsDisabled stats;
      #endif
      // ...
    };
    

    The same run-time overhead considerations as in the template case apply: the compiler should be able to completely remove the overhead of introducing a statistics class, assuming that the statistics classes' functions are inline or have internal linkage. The modern C++ way to enforce internal linkage is to put them into an anonymous namespace { }. Note that this requires your fmpool definition to be internal to the compilation unit and not in a header.

  • 2
    If you know all valid template arguments (StatisticsEnabled, StatisticsDisabled), you can use extern template and leave the implementation in a .cpp. This way, there is no compile-time overhead for the template version. – pschill Jul 16 at 8:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.