I have a class called Shape
, which can be initialized from any iterable, and a class called Array
, which simply contains a Shape
. However, I'm getting a compile error I can't explain when I try to initialize an Array
:
class Shape
{
public:
template<typename Iterator>
Shape(Iterator first, Iterator last)
: m_shape(first, last) {}
template <typename Iterable>
Shape(const Iterable& shape)
: Shape(shape.begin(), shape.end()) {}
template<typename T>
Shape(std::initializer_list<T> shape)
: Shape(shape.begin(), shape.end()) {}
private:
std::vector<std::size_t> m_shape;
};
class Array
{
public:
Array(const Shape& shape)
: m_shape(shape) {}
private:
Shape m_shape;
};
int main() {
Shape s{0}; // ok
Array a1({1, 2}); // ok
Array a2({0}); // error
}
The compilation error appears on the second constructor of Shape
:
prog.cxx:35:16: required from here
prog.cxx:14:23: error: request for member ‘begin’ in ‘shape’, which is of non-class type ‘const int’
: Shape(shape.begin(), shape.end()) {}
~~~~~~^~~~~
prog.cxx:14:38: error: request for member ‘end’ in ‘shape’, which is of non-class type ‘const int’
: Shape(shape.begin(), shape.end()) {}
~~~~~~^~~
I don't understand what is happening here. Why is the Iterable
constructor called instead of the initializer_list<T>
constructor? What's the difference between the Shape
constructor with {0}
and the Array
constructor?
The code is ill-formed, but not for the reason gcc claims it is. When you write:
Array a2({0});
We do overload resolution over all the constructors of Array
using the initializer {0}
.
Option #1 is:
Array(Shape const& );
on which we would recurse into attempting to copy-initialize Shape
with {0}
which ends up invoking the std::initializer_list<int>
constructor template due to preferential treatment of std::initializer_list
during list-initialization.
However, that's just one option. Option #2 is:
Array(Array&& );
The implicit move constructor. To check if that's a candidate, we see if we can initialize Array
with {0}
, which basically starts over again. In this next layer, we see if we can initialize Shape
with 0
(since we're one layer removed), and we can - that's your accept-all-the-things constructor template. This does involve two user-defined conversion sequences, but that's ok for list-initialization.
So we have two options:
{0} --> Shape
0 --> Shape --> Array
Neither is better than the other, so the call is ambiguous.
The easy fix is to add a constraint to your constructor template such that it actually is a range. This is generally good practice anyway, since you don't want is_constructible_v<Shape, int>
to be true...