A colleague of mine has find a strange compilation error with GCC when one tries to brace initialize a std::vector<Foo>
with another, if Foo
is move-only but provides a templated constructor, like in the following example:
#include <vector>
struct Foo{
template <class T> Foo(T) {}
Foo(const Foo&) = delete;
Foo(Foo&&) = default;
};
int main(){
std::vector<Foo> v1(std::vector<Foo>{}); // ok
std::vector<Foo> v2{std::vector<Foo>{}}; // compilation error
}
(See code and error on Compiler Explorer.)
Is it a bug? If so, where (within the instantiation process, I guess) is GCC making the mistake? In other words, since the code is ok for all three compilers as soon as one removes the templated construtor, what in the latter trips GCC up?
CodePudding user response:
If we stop using vector so we can see what constructors are getting used, then maybe it gets more clear what's going on.
template <typename T>
struct V {
V() { std::cout << "V Default\n"; }
V(const V&) { std::cout << "V Copy\n"; }
V(V&&) { std::cout << "V Move\n"; }
V(std::initializer_list<T>) { std::cout << "V list<" << typeid(T).name() << ">\n"; }
};
struct Foo {
template <class T>
Foo(T) { std::cout << "Foo <" << typeid(T).name() << ">\n"; }
Foo(const Foo&) = delete;
Foo(Foo&&) { std::cout << "Foo Move\n"; }
};
https://godbolt.org/z/9qs9bvjjz
Now let's try to copy construct a V<Foo>
from a V<Foo>
, using both ()
and {}
.
V<Foo> v0{};
V<Foo> v1(v0);
V<Foo> v2{v1};
To create v1
with both gcc and clang, we get "V Copy", copy ctor of V chosen as best. For v2
on clang we get "V Copy" as well, but for gcc, we get "V list<Foo>". It's using the initializer list constructor of V instead of the copy constructor.
Now, one should wonder, how did we get a std::initializer_list<Foo>
from a list of one element of V<Foo>
? That's because of the template constructor of Foo
, which provides a conversion constructor to allow creating a Foo
from a V<Foo>
.
gcc acts the same as if we had written:
V<Foo> v2( { Foo(v1) } );
I think gcc is correct here.
V<Foo> v2{v1}
is direct-list-initialization and the resolution is done in two phases. The first is to consider only std::initializer_list
constructors, then the second is to consider all constructors with the list as the arguments. The first phase should produce a match, by converting v1
into a Foo
, and that's what gcc picks.
In case it's not clear, once the initializer_list constructor is chosen, you get an error with vector because it's not possible to use that constructor with a list of non-copyable objects. The initializer_list's elements are const and the vector is copy-initialized from it.
CodePudding user response:
Is it a bug?
I think that GCC is correct in rejecting the program for the reasons described below:
std::vector
has a constructor that takes an std::initializer_list
:
vector (initializer_list<value_type> il, const allocator_type& alloc = allocator_type());
Thus when you wrote:
std::vector<Foo> v2{std::vector<Foo>{}};
In the above statement, the quoted initializer list ctor of vector
can be used/utilised.
Moreover, the copy constructor Foo::Foo(const Foo&)
will be used here since from dcl.init.lst:
An object of type
std::initializer_list<E>
is constructed from an initializer list as if the implementation allocated a temporary array ofN
elements of typeconst E
, whereN
is the number of elements in the initializer list. Each element of that array is copy-initialized with the corresponding element of the initializer list, and the std::initializer_list object is constructed to refer to that array.
(emphasis mine)
But since the copy constructor is deleted
we get the mentioned error. You can confirm that this is indeed the case by not deleting the copy constructor as done in this demo and as we can notice there the program works(compiles) without any issues.