1

I've a bunch of code, which is cluttered with preprocessor blocks, like #ifdef FEATURE_A, #ifdef _MSC_VER and so on.

I would like to refactor some of the code in order to replace some preprocessor blocks by template implementations.

EDIT: The task is not to remove all preprocessor blocks, but some of them, in order to get rid of the clutter.

I don't want to bore you with foobar examples, so here's one from the real world (not my code):

template <typename T>
std::string demangle()
{
#ifdef __GNUC__
  size_t sz;
  int status;
  char* ptr = abi::__cxa_demangle(typeid(T).name(), 0, &sz, &status);
  std::string name(ptr ? ptr : "", ptr ? strlen(ptr) : 0);
  if(ptr){ free(ptr); }
  std::string::size_type pos = name.rfind("::");
  if(pos != std::string::npos)
  {
    name = name.substr(pos + 2);
  }  
#elif _MSC_VER
  std::string name(typeid(T).name());
  static const std::string struct_prefix("struct ");
  static const std::string class_prefix("class ");
  static const std::string ptr_postfix(" *");

  std::string::size_type
  at = name.find(struct_prefix);
  if(at != std::string::npos) { name.erase(at, struct_prefix.size()); }
  at = name.find(class_prefix);
  if(at != std::string::npos) { name.erase(at, class_prefix.size()); }
  at = name.find(ptr_postfix);
  if(at != std::string::npos) { name.erase(at, ptr_postfix.size()); }
#else
  std::string name(typeid(T).name());
#endif
  return name;
}

Question 1: How to transform this to an equvalent template implementation?

Question 2: Why is it worth the effort, or why not?

mrAtari
  • 620
  • 5
  • 17
  • You need these preprocessor definitions anyway. Maybe you have three functions `std::string demangle()` (maybe distributed over several files for several compilers). –  Sep 09 '16 at 10:23
  • Yep, you need them anyway, but you can remove the clutter from the implementation. – mrAtari Sep 09 '16 at 10:44
  • Assuming you could replace the preprocessor directives with templates (others say you can't), it is unlikely that such replacement would use less total volume of text. So you'd get something bigger and clumsier, that would work in a surprising way for the reader. After all, C++ coders *do* know what PP directives are. So I don't see how this could be a win. – Ira Baxter Sep 09 '16 at 12:12

2 Answers2

1

UPDATE: As others have pointed out, and the provided real-world example shows it in a beautiful manner, there are cases when it's not possible to get rid of preprocessor. Especially when PP covers platform specifica like abi::__cxa_demangle. However, I'm new to template meta programming, so it's a matter of interest, to find out advantages and drawbacks of this approach.

Here's my own solution, unfortunately it's almost 3 times as long, as the original code. This is mostly because of some helper stuff needed for the solution.

For Question 1:

I used an enum and the Int2Type mapper to tranlate the preprocessor values into user defined types. In the second step the different parts are extracted to partially specialized templates.

Here's the refactored code example (version 2):

#include <iostream>
#include <cstring>
#include <stdlib.h>
#include <typeinfo>


enum CompilerIds 
{
    ANY = 0,
    MSC = 1,
    GNUC = 2
};

template <int v>
struct Int2Type 
{   
    const static int value= v; 
};

#ifdef __GNUC__
 #include <cxxabi.h>
 typedef Int2Type<GNUC> CompilerType;
#elif _MSC_VER
 namespace abi
 {
    char* __cxa_demangle(const char* name, int n, size_t* sz, int* status);
 }
 typedef Int2Type<MSC> CompilerType;
#else
 typedef Int2Type<ANY> CompilerType;
#endif

template <int N> 
struct compiler_traits
{
    static std::string demangle(std::string name)
    {

        return name;
    }
};

template <>
struct compiler_traits<GNUC> 
{
    static std::string demangle(std::string name)
    {
        size_t sz;
        int status;
        char* ptr = abi::__cxa_demangle(name.c_str(), 0, &sz, &status);
        std::string retName(ptr ? ptr : "", ptr ? strlen(ptr) : 0);
        if(ptr){ free(ptr); }
        std::string::size_type pos = retName.rfind("::");
        if(pos != std::string::npos)
        {
            retName = retName.substr(pos + 2);
        }
        return retName;
    }
};

template <>
struct compiler_traits<MSC >
{
    static std::string demangle(std::string name)
    {
        static const std::string struct_prefix("struct ");
        static const std::string class_prefix("class ");
        static const std::string ptr_postfix(" *");

        std::string::size_type
        at = name.find(struct_prefix);
        if(at != std::string::npos) { name.erase(at, struct_prefix.size()); }
        at = name.find(class_prefix);
        if(at != std::string::npos) { name.erase(at, class_prefix.size()); }
        at = name.find(ptr_postfix);
        if(at != std::string::npos) { name.erase(at, ptr_postfix.size()); }

        return name;
    }
};

template <typename T, typename traits = compiler_traits<CompilerType::value> >
struct demangle
{
    static std::string Exec()
    {
        return traits::demangle(typeid(T).name());
    }
};


int main() 
{
    std::cout << "\n mangled:" << typeid(Int2Type<GNUC>).name();
    std::cout << "\n demangled:" << demangle<Int2Type<GNUC> >::Exec() <<"\n";

    std::cout << "\n instatiate the msc version:" << compiler_traits<MSC>::demangle("struct Int2Type<2>") <<"\n";

    return 0;
}

The output for gcc 4.9.2 is:

mangled:8Int2TypeILi2EE
demangled:Int2Type<2>

instatiate the msc version:Int2Type<2>

I had to hack the abi::__cxa_demangle with a forward declaration for the MS compiler (would never do that in production code). You will see, that instantiation of compiler_traits<GNUC> on a MS system will fail.

For Question 2:

It is possible to replace PP blocks with templates in principle, but may also have some serious drawbacks.

I still tend to give it a chance, that it's worth the effort, especially when you have not "platform stuff", but feature switch stuff like #ifdef FEATURE_Aand so on.

With templates:

  • Extension is very easy and will not break OCP
  • It improves readabilty, especially if you have many #ifdef's
  • Otherways unreachable code becomes testable
mrAtari
  • 620
  • 5
  • 17
1

It's not possible. C++ templates use two-phase lookup (Two phase name lookup for C++ templates - Why?), so any names used within a function template that do not depend on a template parameter must be available at the point of declaration.

Your GCC implementation uses the name abi::__cxa_demangle, so any implementation that does not provide that name should reject your code (some may not, but only because they don't implement two-phase lookup correctly: What exactly is "broken" with Microsoft Visual C++'s two-phase template instantiation?).

The only way to work around this is to contain the use of abi::__cxa_demangle within a preprocessor #ifdef block, which effectively implies using your original implementation anyway.

Community
  • 1
  • 1
ecatmur
  • 152,476
  • 27
  • 293
  • 366