Search code examples
c++c++17template-meta-programmingsfinae

c++ Use std::enable_if to conditionally add getters to a variadic variant template


I am trying to add specializations for the case where my variant has any of int, float, bool, and others as template arguments.

My attempt so far is:

#include <iostream>
#include <variant>
#include <string>
#include <type_traits>

template<typename... Types>
struct variant : std::variant<Types...> {

    using std::variant<Types...>::variant;

    template<typename T>
    const T& get() const { return std::get<T>(*this); }

#define VARGET(X) typename std::enable_if<(std::is_same<Types, X>::value ||  ... ), X>::type get_##X() const { return get<X>(); }

    VARGET(int)
    VARGET(float)
    VARGET(double)
    VARGET(bool)
    using std_string = std::string;
    VARGET(std_string)
};

int main()
{

    variant<int, float, bool> var = 3;
    std::cout << var.get_int() << std::endl;

    variant<int, float, bool> var2 = 0.1f;
    std::cout << var2.get_float() << std::endl;

    return 0;
}

But on gcc 9 this gives the error:

error: failed requirement 'std::is_same<int, double>::value || std::is_same<float, double>::value || std::is_same<bool, double>::value'; 'enable_if' cannot be used to disable this declaration

Why this confuses me:

From my understanding of SFINAE, when one of the arguments is one of the specified types, the macro template evaluates to: (For the example of int)

std::enable_if<(std::is_same<Types, int>::value || ... ), int>::type evaluates to int

so the expression becomes int get_int() const { std::get<int>(*this) }

And if it is not one of the specified types:

std::enable_if<(std::is_same<Types, size_t>::value || ... ), size_t>::type evaluates to nothing

so the expression becomes get_size_t() const { std::get<size_t>(*this) }

This is invalid syntax because the function does not have a return type, but because of SFINAE this should still compile because of other substitutions of X do produce valid syntax.

What is wrong with my code, and is there a way to get the result I desire?

Thanks.


Solution

  • The error I am receiving when trying to compile your code differs from yours, see on Compiler Explorer; I receive the error "no type named type in ..." which I would have expected, as SFINAE does not apply in this case.

    SFINAE would apply for instance in a function call if there is a substitution which whould cause a failure. Nothing were to be substituted in the function, the code is always wrong / always correct depending on the struct's template arguments.

    When creating the struct, it is either always malformed, or always well-formed. You might be able to work around this by imposing an artificial substitution in the function:

    template <bool f=false>
    std::enable_if_t<(std::is_same<Types, int>::value ||  ...  ||f ), int> get_int() const { return get<int>(); }
    

    Then it should compile fine.

    Substitution occurs in

    • all types used in the function type (which includes return type and the types of all parameters)
    • all types used in the template parameter declarations
    • all expressions used in the function type
    • all expressions used in a template parameter declaration (since C++11)
    • all expressions used in the explicit specifier (since C++20)

    The reason why the code is condensed best in the fact, that std::enable_if_t<false, int> func() {} will always fail to compile.