Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ handling specific impl - #ifdef vs private inheritance vs tag dispatch

Tags:

c++

oop

I have some classes implementing some computations which I have to optimize for different SIMD implementations e.g. Altivec and SSE. I don't want to polute the code with #ifdef ... #endif blocks for each method I have to optimize so I tried a couple of other approaches, but unfotunately I'm not very satisfied of how it turned out for reasons I'll try to clarify. So I'm looking for some advice on how I could improve what I have already done.

1.Different implementation files with crude includes

I have the same header file describing the class interface with different "pseudo" implementation files for plain C++, Altivec and SSE only for the relevant methods:

// Algo.h
#ifndef ALGO_H_INCLUDED_
#define ALGO_H_INCLUDED_
class Algo
{
public:
    Algo();
    ~Algo();

    void process();
protected:
    void computeSome();
    void computeMore();
};
#endif

// Algo.cpp
#include "Algo.h"
Algo::Algo() { }

Algo::~Algo() { }

void Algo::process()
{
    computeSome();
    computeMore();
}

#if defined(ALTIVEC)
#include "Algo_Altivec.cpp" 
#elif defined(SSE)
#include "Algo_SSE.cpp"
#else
#include "Algo_Scalar.cpp"
#endif

// Algo_Altivec.cpp
void Algo::computeSome()
{
}
void Algo::computeMore()
{
}
... same for the other implementation files

Pros:

  • the split is quite straightforward and easy to do
  • there is no "overhead"(don't know how to say it better) to objects of my class by which I mean no extra inheritance, no addition of member variables etc.
  • much cleaner than #ifdef-ing all over the place

Cons:

  • I have three additional files for maintenance; I could put the Scalar implementation in the Algo.cpp file though and end up with just two but the inclusion part will look and fell a bit dirtier
  • they are not compilable units per-se and have to be excluded from the project structure
  • if I do not have the specific optimized implementation yet for let's say SSE I would have to duplicate some code from the plain(Scalar) C++ implementation file
  • I cannot fallback to the plain C++ implementation if nedded; ? is it even possible to do that in the described scenario ?
  • I do not feel any structural cohesion in the approach

2.Different implementation files with private inheritance

// Algo.h
class Algo : private AlgoImpl
{
 ... as before
}

// AlgoImpl.h
#ifndef ALGOIMPL_H_INCLUDED_
#define ALGOIMPL_H_INCLUDED_
class AlgoImpl
{
protected:
    AlgoImpl();
    ~AlgoImpl();

   void computeSomeImpl();
   void computeMoreImpl();
};
#endif

// Algo.cpp
...
void Algo::computeSome()
{
    computeSomeImpl();
}
void Algo::computeMore()
{
    computeMoreImpl();
}

// Algo_SSE.cpp
AlgoImpl::AlgoImpl()
{
}
AlgoImpl::~AlgoImpl()
{
}
void AlgoImpl::computeSomeImpl()
{
}
void AlgoImpl::computeMoreImpl()
{
}

Pros:

  • the split is quite straightforward and easy to do
  • much cleaner than #ifdef-ing all over the place
  • still there is no "overhead" to my class - EBCO should kick in
  • the semantic of the class is much more cleaner at least comparing to the above that is private inheritance == is implemented in terms of
  • the different files are compilable, can be included in the project and selected via the build system

Cons:

  • I have three additional files for maintenance
  • if I do not have the specific optimized implementation yet for let's say SSE I would have to duplicate some code from the plain(Scalar) C++ implementation file
  • I cannot fallback to the plain C++ implementation if nedded

3.Is basically method 2 but with virtual functions in the AlgoImpl class. That would allow me to overcome the duplicate implementation of plain C++ code if needed by providing an empty implementation in the base class and override in the derived although I will have to disable that behavior when I actually implement the optimized version. Also the virtual functions will bring some "overhead" to objects of my class.

4.A form of tag dispatching via enable_if<>

Pros:

  • the split is quite straightforward and easy to do
  • much cleaner than #ifdef ing all over the place
  • still there is no "overhead" to my class
  • will eliminate the need for different files for different implementations

Cons:

  • templates will be a bit more "cryptic" and seem to bring an unnecessary overhead(at least for some people in some contexts)
  • if I do not have the specific optimized implementation yet for let's say SSE I would have to duplicate some code from the plain(Scalar) C++ implementation
  • I cannot fallback to the plain C++ implementation if needed

What I couldn't figure out yet for any of the variants is how to properly and cleanly fallback to the plain C++ implementation.

Also I don't want to over-engineer things and in that respect the first variant seems the most "KISS" like even considering the disadvantages.

like image 471
celavek Avatar asked Sep 25 '11 22:09

celavek


1 Answers

You could use a policy based approach with templates kind of like the way the standard library does for allocators, comparators and the like. Each implementation has a policy class which defines computeSome() and computeMore(). Your Algo class takes a policy as a parameter and defers to its implementation.

template <class policy_t>
class algo_with_policy_t {
    policy_t policy_;
public:
    algo_with_policy_t() { }
    ~algo_with_policy_t() { }

    void process()
    {
        policy_.computeSome();
        policy_.computeMore();
    }
};

struct altivec_policy_t {
    void computeSome();
    void computeMore();
};

struct sse_policy_t {
    void computeSome();
    void computeMore();
};

struct scalar_policy_t {
    void computeSome();
    void computeMore();
};

// let user select exact implementation
typedef algo_with_policy_t<altivec_policy_t> algo_altivec_t;
typedef algo_with_policy_t<sse_policy_t> algo_sse_t;
typedef algo_with_policy_t<scalar_policy_t> algo_scalar_t;

// let user have default implementation
typedef
#if defined(ALTIVEC)
    algo_altivec_t
#elif defined(SSE)
    algo_sse_t
#else
    algo_scalar_t
#endif
    algo_default_t;

This lets you have all the different implementations defined within the same file (like solution 1) and compiled into the same program (unlike solution 1). It has no performance overheads (unlike virtual functions). You can either select the implementation at run time or get a default implementation chosen by the compile time configuration.

template <class algo_t>
void use_algo(algo_t algo)
{
    algo.process();
}

void select_algo(bool use_scalar)
{
    if (!use_scalar) {
        use_algo(algo_default_t());
    } else {
        use_algo(algo_scalar_t());
    }
}
like image 83
Bowie Owens Avatar answered Oct 06 '22 00:10

Bowie Owens