OiO.lk Blog templates Using macros to define large number of convenience aliases/constant: is there a better alternative?
templates

Using macros to define large number of convenience aliases/constant: is there a better alternative?


I am writing a constexpr static library, where I have a general_v type that is fairly clunky to use as a type in code, and is intended to be hidden from the end-user. From this, the user-facing aliases are defined, followed by prototypical constants. All of these aliases and constants are templated so that the user can define the underlying floating-point type:

template<std::floating_point T, int... Is>
struct general_v
{
    T value;
}

template<std::floating_point T> using specific_a_v = general_v<T, 1, 0, 0, 0, 0>;
template<std::floating_point T> using specific_b_v = general_v<T, 0, 1, 0, 0, 0>;
template<std::floating_point T> using specific_c_v = general_v<T, 0, 0, 1, 0, 0>;
template<std::floating_point T> using specific_d_v = general_v<T, 0, 0, 0, 1, 0>;
template<std::floating_point T> using specific_e_v = general_v<T, 0, 0, 0, 0, 1>;

template<std::floating_point T> static constexpr specific_a_v<T> prototype_a_v{ static_cast<T>(1.0) };
template<std::floating_point T> static constexpr specific_b_v<T> prototype_b_v{ static_cast<T>(1.0) };
template<std::floating_point T> static constexpr specific_c_v<T> prototype_c_v{ static_cast<T>(1.0) };
template<std::floating_point T> static constexpr specific_d_v<T> prototype_d_v{ static_cast<T>(1.0) };
template<std::floating_point T> static constexpr specific_e_v<T> prototype_e_v{ static_cast<T>(1.0) };

A key goal of this library is that its usage will be concise. To aid this, I want to define convenience aliases and constants for each of the key floating point types:

using specific_a_f = specific_a_v<float>;
using specific_b_f = specific_b_v<float>;
using specific_c_f = specific_c_v<float>;
using specific_d_f = specific_d_v<float>;
using specific_e_f = specific_e_v<float>;

using specific_a = specific_a_v<double>;
using specific_b = specific_b_v<double>;
using specific_c = specific_c_v<double>;
using specific_d = specific_d_v<double>;
using specific_e = specific_e_v<double>;

using specific_a_l = specific_a_v<long double>;
using specific_b_l = specific_b_v<long double>;
using specific_c_l = specific_c_v<long double>;
using specific_d_l = specific_d_v<long double>;
using specific_e_l = specific_e_v<long double>;



static constexpr auto prototype_a_f{ prototype_a_v<float> };
static constexpr auto prototype_b_f{ prototype_b_v<float> };
static constexpr auto prototype_c_f{ prototype_c_v<float> };
static constexpr auto prototype_d_f{ prototype_d_v<float> };
static constexpr auto prototype_e_f{ prototype_e_v<float> };

// and so on

This way, this will save using up some real estate during usage. However, as you can start to see, this is starting to get repetitive fast. If these were all the types, maybe this is fine, but I then having aliases for types that combine the above types, which will likely be added to over time. This is proving to be a bit of a code-management pain: every time I add a new type, I have to add all of the convenience aliases/prototypes for each one.

Macros appear to be a solution, where I can let a macro call add these convenience aliases/prototypes for me:

#define DEFINE_CONVENIENCE_ALIASES(name_of_type)       \
using name_of_type##_f = name_of_type##_v<float>;      \
using name_of_type     = name_of_type##_v<double>;     \
using name_of_type##_l = name_of_type##_v<long double>;\

#define DEFINE_CONVENIENCE_CONSTANTS(name_of_variable)                         \
static constexpr auto name_of_variable##_f = name_of_variable##_v<float>;      \
static constexpr auto name_of_variable     = name_of_variable##_v<double>;     \
static constexpr auto name_of_variable##_l = name_of_variable##_v<long double>;\

template<std::floating_point T> using specific_a_v = general_v<T, 1, 0, 0, 0, 0>;
DEFINE_CONVENIENCE_ALIASES(specific_a)
template<std::floating_point T> using specific_b_v = general_v<T, 0, 1, 0, 0, 0>;
DEFINE_CONVENIENCE_ALIASES(specific_b)
template<std::floating_point T> using specific_c_v = general_v<T, 0, 0, 1, 0, 0>;
DEFINE_CONVENIENCE_ALIASES(specific_c)
template<std::floating_point T> using specific_d_v = general_v<T, 0, 0, 0, 1, 0>;
DEFINE_CONVENIENCE_ALIASES(specific_d)
template<std::floating_point T> using specific_e_v = general_v<T, 0, 0, 0, 0, 1>;
DEFINE_CONVENIENCE_ALIASES(specific_e)

template<std::floating_point T> static constexpr specific_a_v<T> prototype_a_v{ static_cast<T>(1.0) };
DEFINE_CONVENIENCE_CONSTANTS(prototype_a)
template<std::floating_point T> static constexpr specific_b_v<T> prototype_b_v{ static_cast<T>(1.0) };
DEFINE_CONVENIENCE_CONSTANTS(prototype_b)
template<std::floating_point T> static constexpr specific_c_v<T> prototype_c_v{ static_cast<T>(1.0) };
DEFINE_CONVENIENCE_CONSTANTS(prototype_c)
template<std::floating_point T> static constexpr specific_d_v<T> prototype_d_v{ static_cast<T>(1.0) };
DEFINE_CONVENIENCE_CONSTANTS(prototype_d)
template<std::floating_point T> static constexpr specific_e_v<T> prototype_e_v{ static_cast<T>(1.0) };
DEFINE_CONVENIENCE_CONSTANTS(prototype_e)

This appears to be much tidier. It also has the added benefit where new floating point types can be supported by changing only the macro definitions.

However, I am aware that macros do not have a fantastic reputation; from the core guidelines: "Scream when you see a macro that isn’t just used for source control (e.g., #ifdef)", and I’m not sure if this usage really falls under source control…

I’m hesistant to use them unless I have to. So, is this a situation where I’m better off using macros? What unexpected problems might I have to face in doing so? I would rather keep all my code in the realm of actual C++ code, but my understanding is that — since this involves the naming of a large number of convenience aliases and constants — there isn’t going to be much in the way of C++ features that will solve this problem, although I’d be very happy to be proven wrong.



You need to sign in to view this answers

Exit mobile version