Can I list-initialize std::vector with perfect forwarding of the elements?
Asked Answered
B

1

15

I noticed that aggregate list initalization of std::vector performs copy initialization when move is more applicable. At the same time, multiple emplace_backs do what I want.

I could only come up with this imperfect solution of writing a template function init_emplace_vector. It's only optimal for non-explicit single-value constructors, though.

template <typename T, typename... Args>
std::vector<T> init_emplace_vector(Args&&... args)
{
  std::vector<T> vec;
  vec.reserve(sizeof...(Args));  // by suggestion from user: eerorika
  (vec.emplace_back(std::forward<Args>(args)), ...);  // C++17
  return vec;
}

Question

Do I really need to use emplace_back in order to initialize std::vector as efficiently as possible?

// an integer passed to large is actually the size of the resource
std::vector<large> v_init {
  1000,  // instance of class "large" is copied
  1001,  // copied
  1002,  // copied
};

std::vector<large> v_emplaced;
v_emplaced.emplace_back(1000);  // moved
v_emplaced.emplace_back(1001);  // moved
v_emplaced.emplace_back(1002);  // moved

std::vector<large> v_init_emplace = init_emplace_vector<large>(
  1000,   // moved
  1001,   // moved
  1002    // moved
);

Output

Class large produces information about copies/moves (implementation below) and so the output of my program is:

- initializer
large copy
large copy
large copy
- emplace_back
large move
large move
large move
- init_emplace_vector
large move
large move
large move

Large class implementation

My implementation of large is simply a copyable/movable type holding a large resource that warns on copy/move.

struct large
{
  large(std::size_t size) : size(size), data(new int[size]) {}

  large(const large& rhs) : size(rhs.size), data(new int[rhs.size])
  {
    std::copy(rhs.data, rhs.data + rhs.size, data);
    std::puts("large copy");
  }

  large(large&& rhs) noexcept : size(rhs.size), data(rhs.data)
  {
    rhs.size = 0;
    rhs.data = nullptr;
    std::puts("large move");
  }

  large& operator=(large rhs) noexcept
  {
    std::swap(*this, rhs);
    return *this;
  }

  ~large() { delete[] data; }

  int* data;
  std::size_t size;
};

Edit

By using reserve, there is no copy or move. Only large::large(std::size_t) constructor is invoked. True emplace.

Beanery answered 8/1, 2020 at 23:52 Comment(6)
It's not aggregate-initialization, you are calling the constructor that takes a std::initializer_list.Jersey
std::vector's constructors are a confusing mess IMHO, and if I were you I'd really avoid getting into it if I don't absolutely have to. Plus, if you know you vectors' contents in advance (which seems like it might be the case) - consider an std::array.Perpetual
An operator= which destroys the source is quite unusual and might cause unexpected problems.Servility
init_emplace_vector could be improved with vec.reserve(sizeof...(Args))Irfan
@Servility operator= does not destroy the source. It's the copy-swap idiom.Beanery
I swear I saw a large& operator=(large& rhs) here, however now it's not in the edit history. Yes, large& operator=(large rhs) is fine, of course.Servility
M
11

Can I aggregate-initialize std::vector ...

No. std::vector is not an aggregate, so it cannot be aggregate-initialised.

You may mean list-initialisation instead, in which case:

Can I [list-initialise] std::vector with perfect forwarding of the elements?

No. List-initialisation uses the std::initializer_list constructor and std::initializer_list copies its arguments.

Your init_emplace_vector appears to be a decent solution, although it can be improved by reserving the memory before emplacing the elements.

Michigan answered 9/1, 2020 at 0:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.