Search code examples
c++templatesvariadic-templatessfinae

SFINAE | strange behaviour


I am studying SFINAE and c++ in general. I'm having a strange behaviour with My SFINAE macros (called here "annotations"):

<lang/Annotations.h>

#pragma once
#define ENABLE_IF(y) typename std::enable_if<y,std::nullptr_t>::type
#define IS_REFERENCE(x) std::is_reference<x>::value
#define IS_CONSTRUCTIBLE(...) std::is_constructible<__VA_ARGS__>::value

I made a custom "MY_OBJECT" class, which provides the following constructor:

MY_OBJECT(const char* stringDataPtr) noexcept;

The objective here is the following:

By using a variadic template function, each template argument's type must be checked: if it can be passed to String constructor (std::is_constructible) then a message "is constructible" must be printed, otherwise "is not constructible" must be printed.


The problem

Even by passing int values, my SFINAE method does not get "SFINAE'd" and I always get "Is constructible" message.


<util/SFINAETools.h

namespace SFINAETools {

    enum class SFINAEResult {
        IS_CONSTRUCTIBLE,
        IS_NOT_CONSTRUCTIBLE,
        IS_REFERENCE,
        IS_NOT_REFERENCE
    };

    std::ostream& operator<<(std::ostream& out, const SFINAEResult& value) {
    
        static std::unordered_map<SFINAEResult, System::MyString> strings {
            {SFINAEResult::IS_CONSTRUCTIBLE, "IS_CONSTRUCTIBLE"},
            {SFINAEResult::IS_NOT_CONSTRUCTIBLE, "IS_NOT_CONSTRUCTIBLE"},
            {SFINAEResult::IS_REFERENCE, "IS_REFERENCE"},
            {SFINAEResult::IS_NOT_REFERENCE, "IS_NOT_REFERENCE"}
        };
        return out << strings[value];
    }

    class SFINAECallbackHandler : public Object {
    public:
        virtual void onSFINAEResult(const SFINAEResult& result) const = 0;
    };

    template <typename... ARGS, ENABLE_IF(IS_CONSTRUCTIBLE(ARGS...))>
    void executeIfConstructible(const SFINAECallbackHandler& callBackHandler) {

        callBackHandler.onSFINAEResult(SFINAEResult::IS_CONSTRUCTIBLE);
    }

    template <typename... ARGS, ENABLE_IF(!IS_CONSTRUCTIBLE(ARGS...))>
    void executeIfConstructible(const SFINAECallbackHandler& callBackHandler) {

        callBackHandler.onSFINAEResult(SFINAEResult::IS_NOT_CONSTRUCTIBLE);
    }

    template <typename X, ENABLE_IF(IS_REFERENCE(X))>
    void executeIfIsReference(const SFINAECallbackHandler& callBackHandler) {

        callBackHandler.onSFINAEResult(SFINAEResult::IS_REFERENCE);
    }

    template <typename X, ENABLE_IF(!IS_REFERENCE(X))>
    void executeIfIsReference(const SFINAECallbackHandler& callBackHandler) {

        callBackHandler.onSFINAEResult(SFINAEResult::IS_NOT_REFERENCE);
    }
};

MAIN.cpp

#include <lang/CppApplication.h>
#include <util/SFINAETools.h>

class MyCallbackHandler :public SFINAETools::SFINAECallbackHandler {

public:

    virtual void onSFINAEResult(const SFINAETools::SFINAEResult& result) const override {

        std::cout << result << std::endl;
    }
};

class MY_OBJECT : public Object {
public:

    MY_OBJECT(const char* strDataPtr) {

    }
};
class Main : public CppApplication {

public:

    virtual int main(const std::vector<String>& arguments) override {

        createString(1, "2");
        return 0;
    }

    template <typename Arg1>
    void createString(Arg1&& arg1) {

        SFINAETools::executeIfConstructible<MY_OBJECT, Arg1>(MyCallbackHandler());
    }

    template <typename Arg1, typename ...Args>
    void createString(Arg1&& arg1, Args&&... args) {

        createString(arg1);
        createString(args...);
    }

    template <typename ...Args>
    void createString(Args&&... args) {

        std::list<MY_OBJECT> list;
        createString(list, args...);
    }
};

Solution

  • I don't know if it is the problem (the only problem) but this macro

    #define ENABLE_IF(y) typename std::enable_if<y>::type
    

    becomes void when y is true.

    So when y is true

    template <typename... Types, ENABLE_IF(!IS_CONSTRUCTIBLE(Types...)) = 0>
    

    becomes

    template <typename... Types, void = 0>
    

    That can't work. 0 isn't a valid value for void. There are no valid values for void.

    And

    template <typename X, ENABLE_IF(IS_REFERENCE(X))>
    

    becomes

    template <typename X, void>
    

    That is even worse.

    I suppose you can define ENABLE_IF to return (in this case) an int

    // ...........................................VVVVV
    #define ENABLE_IF(y) typename std::enable_if<y, int>::type
    

    remembering the = 0 after every ENABLE_IF


    Another problem: now you have

    template <typename X, typename Y>
    static bool executeIfConstructible(std::function<void()> predicate) {
    
        predicate();
        return true;
    }
    
    template <typename X, typename Y , ENABLE_IF(!IS_CONSTRUCTIBLE(X,Y))>
    static bool executeIfConstructible(std::function<void()> predicate) {
    
        return false;
    }
    

    So you have two version of executeIfContructible(): the first one always enabled, the second one enabled only when !IS_CONSTRUCTIBLE(X,Y) is true.

    You have to disable the first one when !IS_CONSTRUCTIBLE(X,Y) is false (when IS_CONSTRUCTIBLE(X,Y)) or you'll have an ambiguous call when the second one is enabled.

    template <typename X, typename Y , ENABLE_IF(IS_CONSTRUCTIBLE(X,Y))>
    static bool executeIfConstructible(std::function<void()> predicate) {
        predicate();
        return true;
    }
    
    template <typename X, typename Y , ENABLE_IF(!IS_CONSTRUCTIBLE(X,Y))>
    static bool executeIfConstructible(std::function<void()> predicate) {
        return false;
    }
    

    Unrequested suggestion: C-style macros are distilled evil. Avoid C-style macros when you can.

    For example, instead a macro for ENABLE_IF, define a using

     template <bool B>
     using EnableIf = typename std::enable_if<B, int>::type;
    

    For IS_REFERENCE and IS_CONSTRUCTIBLE—if you are sure you need them—you can define (starting from C++14) a couple of constexpr template variables

     template <bool B>
     constexpr bool IsReference = std::is_reference<B>::value;
    
     template <typename ... Ts>
     constexpr bool IsConstructible = std::is_constructible<Ts...>::value;