braced initialization of std::vector of move-only objects fails [duplicate]
Asked Answered
F

1

6

I can concisely (with braces) initialize 5 out of 6 of the following cases:

of copyable of move-only
array YES YES
std::array YES YES
std::vector YES NO

The one case that doesn't seem to work is attempting initialization of a std::vector of move-only objects; that fails to compile, with message such as: "error: call to implicitly-deleted copy constructor of 'std::unique_ptr'".

Why is this? And is there an alternative initialization syntax that works for that case?

The following program demonstrates.

/*
clang++ -std=c++14 -W -Wall -Werror question.cc -o question
clang++ -std=c++17 -W -Wall -Werror question.cc -o question
clang++ -std=c++20 -W -Wall -Werror question.cc -o question
clang++ -std=c++2b -W -Wall -Werror question.cc -o question
g++ -std=c++14 -W -Wall -Werror question.cc -o question
g++ -std=c++17 -W -Wall -Werror question.cc -o question
g++ -std=c++20 -W -Wall -Werror question.cc -o question
g++ -std=c++2b -W -Wall -Werror question.cc -o question
*/
#include <array>
#include <memory>
#include <vector>

int main(int, char **) {
  {
    // I can initialize an array, std::array, or std::vector of
    // copyable objects concisely as follows:
    std::shared_ptr<int> a[] = {std::make_shared<int>(42)};
    std::shared_ptr<int> b[] {std::make_shared<int>(42)};
    std::array<std::shared_ptr<int>, 1> c = {std::make_shared<int>(42)};
    std::array<std::shared_ptr<int>, 1> d {std::make_shared<int>(42)};
    std::vector<std::shared_ptr<int>> e = {std::make_shared<int>(42)};
    std::vector<std::shared_ptr<int>> f {std::make_shared<int>(42)};
  }
  {
    // And I can also initialize an array or std::array (but not a std::vector)
    // of move-only (non-copyable) objects similarly:
    std::unique_ptr<int> a[] = {std::make_unique<int>(42)};
    std::unique_ptr<int> b[] {std::make_unique<int>(42)};
    std::array<std::unique_ptr<int>, 1> c = {std::make_unique<int>(42)};
    std::array<std::unique_ptr<int>, 1> d {std::make_unique<int>(42)};

    // But if I try to do the same with std::vector of move-only
    // objects, it fails to compile.
    // clang++:
    //   "error: call to implicitly-deleted copy constructor
    //    of 'std::unique_ptr<int>'"
    // g++ (c++14, c++17):
    //     "error: static assertion failed: result type must be constructible
    //     from input type"
    // g++ (g++20, g++2b):
    //     "error: use of deleted function
    //      ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(
    //      const std::unique_ptr<_Tp, _Dp>&)
    //      [with _Tp = int; _Dp = std::default_delete<int>]’
#if 1  // (hard code to 1 for error, 0 for successful compile)
    std::vector<std::unique_ptr<int>> e = {std::make_unique<int>(42)};
    std::vector<std::unique_ptr<int>> f {std::make_unique<int>(42)};
#endif
    // The most concise workaround I have found is as follows.
    // (This would not work if I wanted to make the vector const.)
    // Notice that initializer-list initialization does succeed,
    // as long as the initializer list is empty!
    std::vector<std::unique_ptr<int>> e_workaround = {};
    e_workaround.push_back(std::make_unique<int>(42));
    std::vector<std::unique_ptr<int>> f_workaround {};
    f_workaround.push_back(std::make_unique<int>(42));
  }

  return 0;
}
Fabri answered 14/8, 2023 at 7:6 Comment(2)
Or vector v(from_range, views::single(make_unique<int>(42)) | views::as_rvalue); :)Syl
Relevant question: initializer_list and move semantics. It seems to be still valid.Parturition
F
7

The constructor you attempt to call is (https://en.cppreference.com/w/cpp/container/vector/vector)

vector( std::initializer_list<T> init, const Allocator& alloc = Allocator() );

(constexpr since C++20)

The argument is a std::initializer_list<T>, by value. Your call is no different from:

auto u = std::make_unique<int>(42);
std::vector<std::unique_ptr<int>> e = {u};

If this would move from u, it would be a bad surprise. On the other hand, moving the std::initializer_list would be of little use, because its just a lightweight proxy to an array of T (copying a std::initializer_list does already not copy the elements).

Your code for std::array and raw c-arrays works, because it is aggregate initialization. No std::initializer_list constructor is involved there.

The workarounds look ok. You can write a function to initialize the vector

/*const*/ std::vector<std::unique_ptr<int>> e = from_ints({42,2,31});

This would also work when the vector is const.

Fiche answered 14/8, 2023 at 7:20 Comment(5)
But I guess from_ints would precisely do what the "most concise workaround" in the question does, right? (I mean, std::vector<NonCopyable> v{}; v.push_back(NonCopyable{whatever});.)Lieu
You say "Your call is no different from: auto u = std::make_unique<int>(42); std::vector<std::unique_ptr<int>> e = {u}; If this would move from u, it would be a bad surprise." Are you sure? I expect your code to non-surprisingly fail to compile, with message something like "not an rvalue". Whereas my initializer is an rvalue std::make_unique<int>(42). So it seems to me the correct transcription would involve std::move to cast your u to an rvalue: ... std::vector<std::unique_ptr<int>> e = {std::move(u)}; In which case moving from u would not be a surprise at all.Fabri
@DonHatch that was only hypothetical speaking. No matter how you put it, the argument is passed by value and it is a std::initializer_list<T> an initializer list of values, nothing is moved when you call that constructor. std::initializer_list doesnt need a move because copying it does not copy the elements, but the elements are T not T&&Fiche
Element of std::initializer_list are const. and moving const lead to copy (unless constructor taking const Obj&&).Phenomena
Possible duplicate: #75796539Bosket

© 2022 - 2024 — McMap. All rights reserved.