How to properly use std::enable_if on a constructor
Asked Answered
C

1

1

This question combines several pieces of code and is a bit complicated, but I tried slimming it down as much as possible.

I am trying to use std::enable_if to conditionally invoke the correct constructor as a result of ambiguous function signatures when a lambda expression is used as input, but the parameters of said lambda expression can be implicitly convertible to one another.

This is an attempt to build upon the following question: Here, but is sufficiently different and focuses on std::enable_if to merit another question. I am also providing the Live Example that works with the problem parts commented out.

To inspect the argument (and result) types of the functor, I have the following class:

template <typename T>
struct function_traits
    : public function_traits<decltype(&T::operator())>
{};
// For generic types, directly use the result of the signature of its 'operator()'

template <typename ClassType, typename ReturnType, typename... Args>
struct function_traits<ReturnType(ClassType::*)(Args...) const>
    // we specialize for pointers to member function
{
    enum { num_args = sizeof...(Args) };

    typedef ReturnType result_type;

    template <size_t N>
    struct arg
    {
        typedef typename std::tuple_element<N, std::tuple<Args...>>::type type;
        // the i-th argument is equivalent to the i-th tuple element of a tuple
        // composed of those arguments.
    };
};

Then I try to run the below code, however, the std::enable_if part does not seem to work, but I know that everything within the brackets does (or should) work as demonstrated by the Live Example.

template<typename data_type, typename Type1, typename Type2>
class A
{
public:
    using a_type = std::tuple<Type1, Type2>;
    using b_type = std::tuple<std::size_t,std::size_t>;

    template<typename Lambda, typename = std::enable_if_t<std::is_same<typename function_traits<Lambda>::arg<0>::type, b_type>::value>>
    A(const Lambda& Initializer)
    {
        std::cout << "idx_type" << std::endl;
    }
    template<typename Lambda, typename = std::enable_if_t<std::is_same<typename function_traits<Lambda>::arg<0>::type, a_type>::value>>
    A(const Lambda& Initializer)
    {
        std::cout << "point_type" << std::endl;
    }
};

int main()
{
    auto f = [](std::tuple<long long, int>) -> double { return 2; };

    std::cout << std::is_same<typename function_traits<decltype(f)>::arg<0>::type, std::tuple<std::size_t, std::size_t>>::value
        << std::is_same<typename function_traits<decltype(f)>::arg<0>::type, std::tuple<long long, int>>::value;

    auto a = A<double, long long, int>{
        [](std::tuple<long long, int>) -> double { return 1; }
    };

    auto b = A<double, long long, int>{
        [](std::tuple<std::size_t, std::size_t>) -> double { return 2; }  
    };

}

So what am I missing? I am working off example #5 here.

Carbamate answered 14/3, 2018 at 20:20 Comment(7)
Wait a minute, do you mean you add a class to the std namespace? Besides a very few exceptions, doing that is explicitly undefined behavior.Lend
Yes, I've been told not to do so, but it is not what makes or breaks this specific issue. Just some legacy stuff that I forgot to clean up.Carbamate
typename = std::enable_if_t<cond> should be std::enable_if_t<cond, bool> = false.Mafia
std::enable_if is designed to be used to initialize default constructor or function parameter at compile time. So that constructor or function will or will not be chosen from the list of possible constructor/functions of the same name according to the result of the compile time expression evaluation. Take a look into std::shared_ptr class, it uses this technique.Messick
I removed the offending std from function_traits and also I tried std::enable_if_t<cond, bool> = false but it doesn't work. @Jarod42, what do you mean?Carbamate
@Carbamate That is the major problem, you have other problems, like the missing of template for dependent name.Cauley
@PasserBy: see default-template-argument-when-using-stdenable-if.Mafia
B
4

Dependent names

typename function_traits<Lambda>::template arg<0>::type
                                  ^^^^^^^^

See this post for more information on dependent names and when template or typename is needed.

enable_if

typename = std::enable_if_t<condition>

should instead be

std::enable_if_t<condition>* = nullptr

as @Jarod42 mentioned. This is because the constructors would otherwise be identical and unable to be overloaded. That their default values differ doesn't change this fact. See this for more information.

Putting it together is

template<typename Lambda, std::enable_if_t<std::is_same_v<typename function_traits<Lambda>::template arg<0>::type, a_type>>* = nullptr>
A(const Lambda&);

Live

Side note

function_traits won't work with either overloaded or templated operator(), it can instead be replaced

template<typename T, typename... Args>
using return_type = decltype(std::declval<T>()(std::declval<Args>()...));

template<typename T, typename... Args>
using mfp = decltype(static_cast<return_type<T, Args...>(T::*)(Args...) const>(&T::operator()));

template<typename Lambda, mfp<Lambda, a_type> = nullptr>
A(const Lambda&);

To check if the callable can be called with the exact arguments without conversions.

Breban answered 14/3, 2018 at 21:5 Comment(2)
Thanks for the detailed explanation, this looks phenomenal! Once I wake up again, I'm sure I'll learn 10 new things.Carbamate
So this did work for me, however, VC++ is still quite picky in how one can use types from derived classes. In my original usage a_type and b_type are actually derived, and further composed of other types. In gcc I can just do Parent::a_type and be done, in VC++ I need to override the parent's types with itself, and cannot use anything from the parent class when defining the child's type. Luckily, it is not a design breaking problem, but a bit annoying. Thanks for the code.Carbamate

© 2022 - 2024 — McMap. All rights reserved.