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_A
and 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