0

I can't understand why the following code doesn't compile. The error message given by the compiler isn't that helpful either.

Working example:

#include <string>
#include <type_traits>

template <typename T>
struct TIteratorValue {
    using Type = typename T::Type;
};

template <typename T>
struct TIteratorValue<T *> {
    using Type = T;
};

template <typename T>
using IteratorValue = typename TIteratorValue<T>::Type;

template <typename T>
struct Test {
    template <typename V,
              std::enable_if_t<std::is_constructible_v<T, V>, int> = 0>
    Test(std::initializer_list<V> const list, size_t const x = 0)
        : Test(list.begin(), list.end(), x) {}

    template <typename I,
              std::enable_if_t<std::is_constructible_v<T, IteratorValue<I>>, int> = 0>
    // Does not compile!
    Test(I begin, I const end, size_t const x = 0) {}
    // Compiles!
    //Test(I begin, I const end, size_t const x) {}
};

int
main() {

    Test<std::string> test({ "a", "b", "c" }, 10);

    return 0;
}

Clang's error message:

C:\Users\joaom\Dropbox\++A\so\weird_overloading.cpp:6:24: error: type 'int' cannot be used prior to '::' because it has no members     
        using Type = typename T::Type;
                              ^
C:\Users\joaom\Dropbox\++A\so\weird_overloading.cpp:15:1: note: in instantiation of template class 'TIteratorValue<int>' requested here
using IteratorValue = typename TIteratorValue<T>::Type;
^
C:\Users\joaom\Dropbox\++A\so\weird_overloading.cpp:25:50: note: in instantiation of template type alias 'IteratorValue' requested here
                          std::enable_if_t<std::is_constructible_v<T, IteratorValue<I>>, int> = 0>
                                                                      ^
C:\Users\joaom\Dropbox\++A\so\weird_overloading.cpp:27:2: note: while substituting prior template arguments into non-type template parameter
      [with I = int]
        Test(I begin, I const end, size_t const x = 0) {}
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
C:\Users\joaom\Dropbox\++A\so\weird_overloading.cpp:35:20: note: while substituting deduced template arguments into function template 'Test'
      [with I = int, $1 = (no value)]
        Test<std::string> test({ "a", "b", "c" }, 10);

The only int is the argument x but I can't see how's that affecting the code. Visual Studio gives me the same error too.

Drew Dormann
  • 59,987
  • 13
  • 123
  • 180
João Pires
  • 927
  • 1
  • 5
  • 16

1 Answers1

1
template <typename I, std::enable_if_t<std::is_constructible_v<T, IteratorValue<I>>, int> = 0>
// Does not compile!
Test(I begin, I const end, size_t const x = 0) {}
// Compiles!
//Test(I begin, I const end, size_t const x) {}

Both of these functions will not compile if the template is instantiated.

Since your main function attempts to construct a Test from 2 parameters, adding a default value to the third parameter simply means that this function should be considered. It allows the template to be instantiated.

I still can't understand why the following code doesn't compile.

You are defining IteratorValue<I> (and therefore I::type) before checking if I is an iterator.

Use the already defined std::iterator_traits to solve this.

// Formatted for clarity
template <typename I,
          std::enable_if_t<
              std::is_constructible_v<
                  T,
                  typename std::iterator_traits<I>::value_type
              >,
              int
          > = 0>
Drew Dormann
  • 59,987
  • 13
  • 123
  • 180
  • If the compiler is picking the 2nd constructor does that mean that it is implicitly converting the brace initializer list to an int? Because the 2nd constructor asks for the first 2 arguments to be of the same type, and one of them is clearly not an int. – João Pires Feb 07 '21 at 21:06
  • 1
    @JoãoPires An argument that's a brace-init list suppresses template parameter deduction (except when the corresponding parameter is `std::initializer_list`), so `I` is deduced from the second argument. The compiler then doesn't get as far as realizing that `{ "a", "b", "c" }` is not convertible to `int`, it errors out earlier. What I still don't understand is why it doesn't simply abandon this overload, due to SFINAE, once it gets to trying `int::type` – Igor Tandetnik Feb 07 '21 at 21:09
  • To me it doesn't make sense that the compiler can't se that in this case, a brace-init list and `int` are clearly not the same type, it should've just skipped that overload altogether... oh well... C++ shenanigans. – João Pires Feb 07 '21 at 21:16
  • @JoãoPires I can't offer a deeper understanding beyond saying that `std::initializer_list` breaks a lot of commonly-held rules. – Drew Dormann Feb 07 '21 at 21:23