12

I have a copy/move probing class:

#include <iostream>

struct A
{
    A()
    {
        std::cout << "Creating A" << std::endl;
    }

    ~A() noexcept
    {
        std::cout << "Deleting A" << std::endl;
    }

    A(const A &)
    {
        std::cout << "Copying A" << std::endl;
    }

    A(A &&) noexcept
    {
        std::cout << "Moving A" << std::endl;
    }

    A &operator=(const A &)
    {
        std::cout << "Copy-assigning A" << std::endl;
        return *this;
    }

    A &operator=(A &&) noexcept
    {
        std::cout << "Move-assigning A" << std::endl;
        return *this;
    }
};

And I have found that running:

#include <vector>

int main(int, char **)
{
    std::vector<A> v { A() };
}

Produces the following output:

Creating A
Copying A
Deleting A
Deleting A

Why won't the initialization just move the objects? I know that std::vector may create undesired copies on resize, but as you can see, adding noexcept did not help here (and besides, I don't think that the reasons a resize causes copies apply to initialization).

If I instead do the following:

std::vector<A> v;
v.push_back(A());

I don't get copies.

Tested with GCC 5.4 and Clang 3.8.

jdehesa
  • 58,456
  • 7
  • 77
  • 121
  • 9
    It's because of the `std::initializer_list` constructor being used. The semantics of that type are unfortunate. – StoryTeller - Unslander Monica Jul 17 '17 at 11:28
  • 3
    This Q/A explains the situation and offers some workarounds: https://stackoverflow.com/questions/8468774/can-i-list-initialize-a-vector-of-move-only-type – M.M Jul 17 '17 at 11:29
  • @StoryTeller @M.M I see, so the problem is not with `std::vector` but with `std::initializer_list`, which apparently cannot forward rvalue references? If someone can please post a proper answer I'll accept it. – jdehesa Jul 17 '17 at 12:49
  • 1
    You can answer your question and accept :) The SO model even encourages it. – StoryTeller - Unslander Monica Jul 17 '17 at 12:50
  • @StoryTeller Thanks for the tip. I know I can answer my own questions, but in this case I feel I'd be taking credit from those who pointed me to the answer. In any case, if no one posts an answer I'll do it myself to resolve the question. – jdehesa Jul 17 '17 at 13:44

1 Answers1

12

This isn't std::vector, but std::initializer_list.

std::initializer_list is backed by a const array of elements. It does not permit non-const access to its data.

This blocks moving from its data.

But this is C++, so we can solve this:

template<class T, class A=std::allocator<T>, class...Args>
std::vector<T,A> make_vector(Args&&...args) {
  std::array<T, sizeof...(Args)> tmp = {{std::forward<Args>(args)...}};
  std::vector<T,A> v{ std::make_move_iterator(tmp.begin()), std::make_move_iterator(tmp.end()) };
  return v;
}

now we get:

auto v = make_vector<A>( A() );

gives you 1 extra move per element:

Creating A
Moving A
Moving A
Deleting A
Deleting A
Deleting A

We can eliminate that extra instance with a careful bit of reserving and emplacing back:

template<class T, class A=std::allocator<T>, class...Args>
std::vector<T,A> make_vector(Args&&...args) {
  std::vector<T,A> v;
  v.reserve(sizeof...(args));
  using discard=int[];
  (void)discard{0,(void(
    v.emplace_back( std::forward<Args>(args) )
  ),0)...};
  return v;
}

Live example of both -- simply swap v2:: for v1:: to see the first one in action.

Output:

Creating A
Moving A
Deleting A
Deleting A

there could be a bit more vector overhead here, as it may be hard for the compiler to prove that emplace_back does not cause reallocation (even though we can prove it), so redundant checks will be compiled in most likely. (In my opinion, we need an emplace_back_unsafe that is UB if there isn't enough capacity).

The loss of the extra set of As is probably worth it.

Another choice:

template<std::size_t N, class T, class A=std::allocator<T>, class...Args>
std::vector<T,A> make_vector(std::array<T, N> elements) {
  std::vector<T,A> v{ std::make_move_iterator(elements.begin()), std::make_move_iterator(elements.end()) };
  return v;
}

which is used like

auto v = make_vector<1,A>({{ A() }});

where you have to specify how many elements manually. It is as efficient as the version 2 above.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524