Search code examples
c++language-lawyerc++20move-semanticsuniform-initialization

Compilation error with GCC when brace-init.ing an instance of a vector of move-only type with templated constructor with another one


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?


Solution

  • 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.