HiveBrain v1.2.0
Get Started
← Back to all entries
patterncppMinor

Use of macros to aid visual parsing of SFINAE template metaprogramming

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
templatevisualsfinaemetaprogrammingmacrosparsinguseaid

Problem

I've recently been introduced to SFINAE to solve the problem of unwanted promotion precedence.

i.e. I was hoping to catch integer types with Foo::Foo(long) and floating-point types with Foo::Foo(double), but alas int -> double rather than long.

```
/*
Constructors for various types, for example, 'Object{"foo"}, Object{42}, Object{3.14} should create a String Long Float respectively

There is a problem with constructors.

Python supports a single integer type, which we wrap with the Long class
And the single floatingpoint type, which we wrap with the Float class

In an ideal world we would just provide two overloads to allow a generic
Object to be initialised as one of these types:

Object(long l) : Object{ Long {l} } { }
Object(double d) : Object{ Float{d} } { }

We want Object{5} to create a Long{5}
Unfortunately, int gets promoted to double, not long ( http://stackoverflow.com/a/27276398/435129 )

Very annoying. Fortunately we can use some SFINAE cunning.
*/
#define DECAY(T) \
typename std::decay::type

#define IS_INTEGRAL(T) \
std::is_integral::value

#define IS_FLOATING(T) \
std::is_floating_point::value

#define SUBFAIL_UNLESS(PRED) \
typename X = typename std::enable_if::type

/*
Note that the first template encounters a substitution failure for any non-integral type,
hence only redirects integral types to init(long(t))

Similarly for floatingpoint types.

Note also the unfortunate use of an empty '...' C parameter expansion.
Without this the compiler will complain that two templates are attempting
to wrap a function with the same signature.

Unfortunately it isn't smart enough to know that the conditions are mutually exclusive.

If you required trapping of three or more such special cases, see NOTE_3_CASES at the bottom of th

Solution

Rule #1 of code formatting: Write readable code.

Rule #2: Don't do anything people tell you not to do... until you know better.

The rules against macros are wise because macros can often be very unintuitive. However, as you have noticed, there are situations where they are very helpful. The problem with macros is that they appear to be a panacea for formatting, and it is not until much later that you realize there were serious fundamental issues.

That being said, they're in the language. They have uses. So if you take the time to understand them, then you can start to use them wisely. The two major issues for macros are:

  • They are global symbols, causing surprise replacements in the most unlikely of places



  • It is easy to write a macro that looks like it does what you want, when it actually compiles into something else.



Global symbols

Nothing is worse than having someone else's symbols screw with your code. If someone defines a macro, and it conflicts with your code, the error is almost always unreadable. Consider the most nefarious one, #define max (a, b) ((a)>(b) ? (a) : (b)), which is defined as part of windows.h. If you've been writing Linux apps, you'll find this suddenly breaks when you had a local named "max."

By your choice to #undef the symbols at the end, it is clear you are aware of this. You did a good job of making sure your defines don't hurt someone else. However, you are still at the whim of anyone who uses the same names as you carelessly. So good job with being careful, but you're not out of the woods.

Surprise compilations

The number on reason why people dislike macros is that they often do things you did not expect. For example, in the max example, a can get evaluated twice. Also, macros and commas don't play well. Macros are generally unaware of brackets, so things like max(getValue(1, 2), getValue(3, 4)) can surprise you with errors. Your code doesn't have any of these, but always be aware of the costs when you're trying to bend code style rules.

However, you do have a few interesting tidbits. For example, did you know that you shouldn't say std::enable_if in a macro? That will do a context-specific search for a namespace named "std." In the wrong situations, that could cause problems. The correct phrasing is ::std::enable_if, which forces it to look in the global scope for the right std.

Think of others

So, in you cases, the macros work. You generally thought it through. Now lets take this code into a business scenario. Developers with much less macro practice than you are going to work on this code. They are going to develop stylistic habits to take parts of how you write. Do you really get enough readability here to warrant potentially confusing code down the line?

Options

So what else can you do? Since macro definitions are usually treated as "hard to read," I feel no qualms writing a few struct to help out. By using structs, I get to avoid the global issues of macros, and they always compile the way you expect them to.

Consider using a template template argument like this:

template  Pred, T>
struct subfail_unless
: std::enable_if::type> >
{ };

template::value >
explicit Object( T&& t      ) { init(long  (t)); }

template::value >
explicit Object( T&& t, ... ) { init(double(t)); }


Why I like this:

  • If you are writing code like this, you MUST know SFINAE, so you aren't going to be threatened by the templating.



  • It you are are just reading this, you don't need to know why it works, you just need to recognize the words that matter.



Consider how few extra characters this has.

macros:              SUBFAIL_UNLESS(     IS_FLOATING       (T))
templates:  typename subfail_unless::value
---------------------------------------------------------------------
difference: typename                std::                     ::value


  • This code can now be reused in a header, instead of needing to #define it everywhere you intend to use it. It is now using only namespaced structs, so it is as safe as any other code.

Code Snippets

template <template <typename> Pred, T>
struct subfail_unless
: std::enable_if<Pred<typename std::decay<T>::type> >
{ };

template<typename T,
         typename subfail_unless<std::is_integral, T>::value >
explicit Object( T&& t      ) { init(long  (t)); }

template<typename T,
         typename subfail_unless<std::is_floating_point, T>::value >
explicit Object( T&& t, ... ) { init(double(t)); }
macros:              SUBFAIL_UNLESS(     IS_FLOATING       (T))
templates:  typename subfail_unless<std::is_floating_point, T>::value
---------------------------------------------------------------------
difference: typename                std::                     ::value

Context

StackExchange Code Review Q#71946, answer score: 6

Revisions (0)

No revisions yet.