Question:
I'm going to great lengths to translate the macros in my projects into constant expressions that can be used with if constexpr
. At the moment I have achieved almost satisfactory results doing some tricks with macros, I start by defining some macros to transform values to text:
#define STRINGIFY(X) #X
#define TO_STRING(X) STRINGIFY(X)
These macros behave surprisingly when passing existing or non-existent definitions, for example the following code:
std::cout << TO_STRING(_DEBUG) << '\n';
It shows _DEBUG
if the macro with the same name is NOT defined, while if it is defined it shows the value of the macro. The type of the resulting value will always be a text literal (due to the #
operator in the STRINGIFY
macro ). I use this trick to create the following enum:
template <int SIZE>
constexpr bool b(const char (&definition)[SIZE])
{
return definition[0] != '_';
}
enum operating_system : bool
{
iOS = b(TO_STRING(__APPLE__)),
Windows = b(TO_STRING(__MINGW32__)),
Linux = b(TO_STRING(__linux__)),
};
With this trick , the macros that are defined will have a true value while those that are not defined the opposite value, so I can write the following code with if constexpr
instead of with #ifdef
:
int main()
{
if constexpr (operating_system::Windows)
{
// Cosas especificas de Windows
}
else if constexpr (operating_system::iOS)
{
// Cosas especificas de iOS
}
// Cosas independientes de sistema operativo.
return 0;
}
I don't like having to delegate to a helper function to translate values (function b
), but it's a lesser evil. The biggest problem with this system is that it is only capable of detecting the presence of macros that begin with an underscore ( _
), it gives false positives for macros whose value is something that begins with an underscore ( _
) and the value of the macro is completely misses as there is no compile-time calculable function that passes text to number (none of my attempts have been successful).
Therefore the following macros (obviously) do not act as expected:
#define _DEBUG 0
#define DRIVERS _09072007
template <int SIZE>
constexpr int i(const char (&definition)[SIZE])
{
return definition[0] != '_'; // que poner aqui?...
}
enum stuff : int
{
cpp_version = i(TO_STRING(__cplusplus)),
debug_enabled = i(TO_STRING(_DEBUG)),
drivers_version = i(TO_STRING(DRIVERS)),
};
int main()
{
std::cout << "C++ version: " << stuff::cpp_version << '\n'
<< "Modo debug: " << stuff::debug_enabled << '\n'
<< "Drivers version: " << stuff::drivers_version << '\n';
return 0;
}
The above code shows:
C++ version: 1 Modo debug: 1 Divers verson: 0
When the ideal would be to have shown:
C++ version: 201500 Modo debug: 0 Divers verson: _09072007
Since __cplusplus
has a numeric value that does not start with an underscore ( _
), it gets the value 1
. The same thing happens to the _DEBUG
macro: it has a value of 0
, which would be like considering that we are not in debug mode but it gets the value 1
. The opposite happens with the macro DRIVERS
, which when starting with an underscore, gets the value 0
.
Ask.
Is there a way to get the desired output? You would need at least one constexpr
that passes literals from text to number.
What have I tried?
I have tried a recursive function, but indexing a text literal is not a constant expression (even with known indexes at compile time).
constexpr int power10(int n)
{
if (n == 0)
return 1;
return 10 * power10(n - 1);
}
template <int SIZE>
constexpr int v(const char (&definition)[SIZE], int INDEX)
{
// error: 'definition' no es una expresion constante
constexpr char c = definition[INDEX];
if (INDEX >= 0)
{
if constexpr (c >= '0' && c <= '9')
{
return v(definition, INDEX - 1) + (power10(SIZE - INDEX - 2) * (c - '0'));
}
else
{
return 0 + v(definition, INDEX - 1);
}
}
return 0;
}
template <int SIZE>
constexpr int f(const char (&definition)[SIZE])
{
return v(definition, SIZE - 2);
}
enum operating_system : bool
{
// error: valor de el enumerador para 'iOS' no es una constante integral
iOS = f(TO_STRING(__APPLE__)),
// error: valor de el enumerador para 'Windows' no es una constante integral
Windows = f(TO_STRING(__MINGW32__)),
// error: valor de el enumerador para 'Linux' no es una constante integral
Linux = f(TO_STRING(__linux__)),
};
Answer:
TO_STRING(X)
only return the name X
if the value is not defined:
std::cout << TO_STRING(__cplusplus) << " " << STRINGIFY(__cplusplus) << '\n'
<< TO_STRING(NO_EXISTO) << " " << STRINGIFY(NO_EXISTO) << '\n';
Exit:
201500L __cplusplus
NO_EXISTO NO_EXISTO
A first approach that occurs to me to solve part of the problem is to verify if the name has an associated value:
template <int SIZE1, int SIZE2 >
constexpr int i(const char (&definition)[SIZE1],const char (&check)[SIZE2])
{
return std::strcmp(definition,check) != 0;
}
Although honestly, for these cases I prefer to use the auto
:
constexpr int i(auto definition, auto check)
{
return std::strcmp(definition,check) != 0;
}
With this we are already able to detect if the name has been defined or not:
#include <iostream>
#include <cstring>
//#define _DEBUG 0
#define DRIVERS 09072007
#define STRINGIFY(X) #X
#define TO_STRING(X) STRINGIFY(X)
#define PARSE(X) TO_STRING(X),#X
constexpr bool i(auto definition, auto check)
{
return std::strcmp(definition,check) != 0;
}
enum stuff : bool
{
cpp_version = i(PARSE(__cplusplus)),
debug_enabled = i(PARSE(DEBUG)),
drivers_version = i(PARSE(DRIVERS)),
};
int main()
{
std::cout << "C++ version: " << stuff::cpp_version << '\n'
<< "Modo debug: " << stuff::debug_enabled << '\n'
<< "Drivers version: " << stuff::drivers_version << '\n';
return 0;
}
Exit:
C++ version: 1
Modo debug: 0
Drivers version: 1
The issue of storing the value is somewhat more complex, since with constexpr
you cannot use the std::string
type since it uses dynamic memory … I'll give you a spin at this point to see if it occurs to me a way to extract the value associated with the name.
This system will fail if the name and value are the same:
#define NO_FUNCIONA NO_FUNCIONA
But the chances of this happening I think are ridiculous.
EDIT:
After chatting and some additional testing I have found a possible solution.
For each name there is a pair of values:
- a boolean indicating whether the name in question is defined or not
- the value of the element (to be taken into account only if the name is defined)
Your most recent solution after my changes are applied:
#include <iostream>
constexpr int power10(int power)
{
if (power == 0)
return 1;
return 10 * power10(power - 1);
}
template<int SIZE_A, int SIZE_B>
constexpr bool same_literals(const char (&a)[SIZE_A], const char (&b)[SIZE_B])
{
bool result = false;
if constexpr( SIZE_A == SIZE_B )
{
result = true;
for (std::int32_t index = 0; result && (index < SIZE_A); ++index)
{
result = (a[index] == b[index]);
}
}
return result;
}
template <int SIZE_A, int SIZE_B>
constexpr std::int32_t integral_value_or_zero(const char (&a)[SIZE_A], const char (&b)[SIZE_B])
{
std::int32_t result = 0;
if ( !same_literals(a, b) )
{
for (std::int32_t index = 0; index < SIZE_B; ++index)
{
if (b[index] >= '0' && b[index] <= '9')
{
result += power10(SIZE_B - index - 2) * (b[index] - '0');
}
}
}
return result;
}
template <int SIZE_A, int SIZE_B>
constexpr bool bool_value_or_false(const char (&a)[SIZE_A], const char (&b)[SIZE_B])
{
return integral_value_or_zero(a,b) != 0;
}
template <int SIZE_A, int SIZE_B>
constexpr const char *string_or_empty(const char (&a)[SIZE_A], const char (&b)[SIZE_B])
{
if (same_literals(a, b))
return "";
return b;
}
enum class Tipos
{
Entero,
Cadena,
};
template<int SIZE_A, int SIZE_B, Tipos>
struct TiposTraits;
template<int SIZE_A, int SIZE_B>
struct TiposTraits<SIZE_A, SIZE_B, Tipos::Entero>
{
static auto Func(const char (&a)[SIZE_A], const char (&b)[SIZE_B])
{ return integral_value_or_zero(a,b); }
};
template<int SIZE_A, int SIZE_B>
struct TiposTraits<SIZE_A, SIZE_B, Tipos::Cadena>
{
static auto Func(const char (&a)[SIZE_A], const char (&b)[SIZE_B])
{ return string_or_empty(a,b); }
};
#define STRINGIFY(X) #X
#define INTEGRAL_VALUE_OR_ZERO(X) integral_value_or_zero(#X, STRINGIFY(X))
#define BOOL_VALUE_OR_FALSE(X) bool_value_or_false(#X, STRINGIFY(X))
#define STRING_OR_EMPTY(X) string_or_empty(#X, STRINGIFY(X))
#define NEW_PAIR(X,T) \
std::make_pair(!same_literals(#X,STRINGIFY(X)),\
TiposTraits<sizeof(#X),sizeof(STRINGIFY(X)),T>::Func(#X,STRINGIFY(X)))
#define _DEBUG FALSE
#define DRIVERS _09072007
enum stuff : int
{
cpp_version = INTEGRAL_VALUE_OR_ZERO(__cplusplus),
debug_enabled = INTEGRAL_VALUE_OR_ZERO(_DEBUG),
drivers_version = INTEGRAL_VALUE_OR_ZERO(DRIVERS),
test = INTEGRAL_VALUE_OR_ZERO(test),
};
namespace properties
{
const std::string cpp_version = STRING_OR_EMPTY(__cplusplus);
const std::string debug_enabled = STRING_OR_EMPTY(_DEBUG);
const std::string drivers_version = STRING_OR_EMPTY(DRIVERS);
const std::string test = STRING_OR_EMPTY(test);
auto const Cpp_version = NEW_PAIR(__cplusplus,Tipos::Cadena);
auto const Driver_version = NEW_PAIR(DRIVERS,Tipos::Entero);
auto const dummy = NEW_PAIR(dummy,Tipos::Cadena);
}
int main()
{
std::cout << "Cpp_version has value: " << std::get<0>(properties::Cpp_version) << '\n'
<< "Cpp_version value: " << std::get<1>(properties::Cpp_version) << '\n'
<< "Driver_version has value: " << std::get<0>(properties::Driver_version) << '\n'
<< "Driver_version value: " << std::get<1>(properties::Driver_version) << '\n'
<< "dummy has value: " << std::get<0>(properties::dummy) << '\n'
<< "dummy value: " << std::get<1>(properties::dummy) << '\n';
return 0;
}
The output that the program generates will now be the following:
Cpp_version has value: 1
Cpp_version value: 201406L
Driver_version has value: 1
Driver_version value: 9072007
dummy has value: 0
dummy value: