4
\$\begingroup\$

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<T>::type

    #define IS_INTEGRAL(T) \
                            std::is_integral< DECAY(T) >::value

    #define IS_FLOATING(T) \
                            std::is_floating_point< DECAY(T) >::value

    #define SUBFAIL_UNLESS(PRED) \
                            typename X = typename std::enable_if<PRED>::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 the file.
    Also http://ideone.com/oDOSLH
    */

public:
    template<typename T, SUBFAIL_UNLESS(IS_INTEGRAL(T)) > explicit Object( T&& t      ) { init(long  (t)); }
    template<typename T, SUBFAIL_UNLESS(IS_FLOATING(T)) > explicit Object( T&& t, ... ) { init(double(t)); }

private:
    void init(long);
    void init(double);

    #undef DECAY
    #undef IS_INTEGRAL
    #undef IS_FLOATING

I know that macros are generally frowned upon. However, in this case they provide a very clear solution, making it very easy for my brain to parse the code.

I would like to know whether in this case the use of macros is justified, and if not what the best alternative would be.

\$\endgroup\$
1
  • \$\begingroup\$ "generally frowned upon" -> don't care about generally; important thing is understanding why. Then you can apply the same logic to your situation. Macros are oft. frowned upon because they can make code harder to read, and runtime code harder to debug. Here, you're making compile-time code easier to read, so... (doesn't necessarily make them the best option, of course) \$\endgroup\$ Commented Dec 7, 2014 at 20:31

3 Answers 3

6
\$\begingroup\$

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:

  1. They are global symbols, causing surprise replacements in the most unlikely of places
  2. 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 <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)); }

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<std::is_floating_point, T>::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.
\$\endgroup\$
4
\$\begingroup\$

With C++ 11 you can replace those macros with an "using alias". In C++ 11 we can perform template aliasing very easily. See: Type alias / Alias template.

\$\endgroup\$
0
\$\begingroup\$

Taking heed to Cort Ammon's critique, I have replaced all of my macros.

I agree that it is ugly to clutter the namespace with #define-s, which might be overwriting others, especially in a header file that could be #include-d in all manner of projects.

I'm happy with the resulting syntax, it feels as though there is no appreciable loss in legibility / clarity, plus syntax highlighting works in XCode (it puts macros in red, which is rather ugly)!

    // old
    #define DECAY(T)        typename std::decay<T>::type

    #define IS_INTEGRAL(T)  std::is_integral< DECAY(T) >::value

    #define IS_FLOATING(T)  std::is_floating_point< DECAY(T) >::value

    #define SUBFAIL_UNLESS(PRED) \
                            typename std::enable_if<PRED, int>::type = 0

    // new
    template< typename T> 
    using decay_t = typename std::decay<T>::type;

    template< bool pred>  
    using subfail_unless_t = typename std::enable_if< pred, int >::type;

    template< typename T>  
    static bool is_integral() { return std::is_integral      < decay_t<T> >::value; }
    template< typename T>  
    static bool is_floating() { return std::is_floating_point< decay_t<T> >::value; }

    template< typename T>  
    using subfail_unless_integral_t = subfail_unless_t< is_integral<T>() >;
    template< typename T>  
    using subfail_unless_floating_t = subfail_unless_t< is_floating<T>() >;

    // - - - 

    // old
    #define IS_OBJECT(T)  \
                        std::is_base_of< Object, T >::value

    #define TEMPLATE_TU \
        template <  typename T,  typename U,  SUBFAIL_UNLESS( IS_OBJECT(T) || IS_OBJECT(U) )  >

    template<typename T, SUBFAIL_UNLESS(IS_INTEGRAL(T)) > 
    explicit Object( T&& t ) : Object{ pyob_from_integral(t) }  { }
    template<typename T, SUBFAIL_UNLESS(IS_FLOATING(T)) > 
    explicit Object( T&& t ) : Object{ pyob_from_floating(t) }  { }


    // new
    template< typename T>  
    static bool is_object() { return std::is_base_of< Object, T >::value; }

    template< typename T, typename U>  
    using subfail_if_neither_is_object_t = 
                        subfail_unless_t< is_object<T>() || is_object<U>() >;

    template<typename T, subfail_unless_integral_t<T> = 0> 
    explicit Object( T&& t ) : Object{ pyob_from_integral(t) }  { }
    template<typename T, subfail_unless_floating_t<T> = 0> 
    explicit Object( T&& t ) : Object{ pyob_from_floating(t) }  { }
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.