3

I'm trying to disable ctor that has non-std::string constructible type. My 1st attempt was like this :

#include <iostream>

struct A
{
    template <typename U, typename = typename std::enable_if<std::is_constructible<std::string, U>::value>::type>
    A(U&& val)
    {
        std::cout << "A(std::string&& val)" << std::string(std::forward<U>(val)) << std::endl;
    }

    template <typename U, typename = typename std::enable_if<not std::is_constructible<std::string, U>::value>::type>
    A(U&& val)
    {
        std::cout << "A(int&& val)" << val << std::endl;
    }
};

int main()
{
    A a1(1);
    A a2("hello");
    A a3(std::string("hello"));
}

But compilation fails in line

A a1(1);

with the following error message :

error C2535: 'A::A(U &&)': member function already defined or declared (live example).

That means, that both conditions for SFINAE succeeded and both ctors are used.

I moved on and tried the following approach :

#include <iostream>

struct A
{
    template <typename U>
    A(U&& val, typename std::enable_if<std::is_constructible<std::string, U>::value>::type* = nullptr)
    {
        std::cout << "A(std::string&& val)" << std::string(std::forward<U>(val)) << std::endl;
    }

    template <typename U>
    A(U&& val, typename std::enable_if<not std::is_constructible<std::string, U>::value>::type* = nullptr)
    {
        std::cout << "A(int&& val)" << val << std::endl;
    }
};

int main()
{
    A a1(1);
    A a2("hello");
    A a3(std::string("hello"));
}

Luckily it compiles and works fine (live example).

So far, Im pretty fine with the 2nd solution but I don't really understand why the 1st approach with enabling/disabling ctor using templated parameter doesn't work.

drus
  • 495
  • 2
  • 6
  • 13

1 Answers1

5

The conditions are not both true, that would be impossible. You can be assured of it by the fact the second approach works, which would not happen if both were still true.

What's important to remember is that default template arguments are not part of the function template's signature. If we somewhat reduce the two c'tors down to their signatures, we'll get this:

template <typename U, typename>
A(U&& val)
{
}

template <typename U, typename>
A(U&& val)
{
}

And the two are identical. So what happens is template argument deduction for U and an attempt to do substitution to see which overload to pick. Even though we can't complete template argument deduction for one of the overloads (the last argument is always unaccounted for in one of them), we still have two templates with identical signatures being found when attempting the deduction process. So the program becomes ill-formed.

The second approach works because the signature itself depends on the enable_if being evaluated. That's why one of the two overloads will always be silently removed, as though it was never there.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • Thanks a lot, Story. I've found a reference that may be helpful to anyone dealing with the same question : [link](https://stackoverflow.com/questions/34369072/sfinae-stdenable-if-as-function-argument) – drus Apr 23 '18 at 08:28
  • @drus - Yup, that example mulls the same issue. The SFINAE needs to be applied on part of the signature. In the linked Q&A, it's the functions return type. A c'tor obviously don't have a return type, so an extra template parameter is used. And I was glad to be of help! – StoryTeller - Unslander Monica Apr 23 '18 at 08:35
  • I would also pay attention to 2 lines from the link I specified. `template ::value, int>::type = 0>` and `template ::value, int>::type = 0>` line that are use to disable ctor. This is tricky here as if _int_ is changed to _float_ that will not compile because `typename float = 0` can't be compiled: [link](https://wandbox.org/permlink/28aKW82Sj5f51c9D). – drus Apr 23 '18 at 09:12