Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can std::vector emplace_back copy construct from an element of the vector itself?

When using push_back of std::vector, I can push an element of the vector itself without fear of invalidating the argument due to reallocation:

std::vector<std::string> v = { "a", "b" };
v.push_back(v[0]); // This is ok even if v.capacity() == 2 before this call.

However, when using emplace_back, std::vector forwards the argument to the constructor of std::string so that copy construction happens in place in the vector. This makes me suspect that reallocation of the vector happens before the new string is copy constructed (otherwise it would not be allocated in place), thus invalidating the argument before use.

Does this mean that I cannot add an element of the vector itself with emplace_back, or do we have some kind of guarantee in case of reallocation, similar to push_back?

In code:

std::vector<std::string> v = { "a", "b" };
v.emplace_back(v[0]); // Is this valid, even if v.capacity() == 2 before this call?
like image 231
rasmus Avatar asked Jul 23 '14 11:07

rasmus


People also ask

Does Emplace_back call copy constructor?

So you can emplace_back does use the desired constructor to create the element and call copy constructor when it need to grow the storage.

What does vector Emplace_back do?

The C++ function std::vector::emplace_back() inserts new element at the end of vector. Reallocation happens if there is need of more space. This method increases container size by one.

Does vector Push_back make a copy?

Yes, std::vector<T>::push_back() creates a copy of the argument and stores it in the vector.

Which is better Push_back or Emplace_back?

The difference in the efficiency of push_back and emplace_back depends on the type of our vector. If the vector is a built-in type, there is no difference between the efficiency of push_back and emplace_back. If the vector type is class or struct, emplace_back is more efficient than push_back.


3 Answers

emplace_back is required to be safe for the same reason push_back is required to be safe; invalidation of pointers and references only has effect once the modifying method call returns.

In practice, this means that emplace_back performing a reallocation is required to proceed in the following order (ignoring error handling):

  1. Allocate new capacity
  2. Emplace-construct new element at the end of the new data segment
  3. Move-construct existing elements into new data segment
  4. Destruct and deallocate old data segment

At this reddit thread STL acknowledges failure of VC11 to support v.emplace_back(v[0]) as a bug, so you should definitely check whether your library supports this usage and not take it for granted.

Note that some forms of self-insertion are specifically prohibited by the Standard; for example in [sequence.reqmts] paragraph 4 Table 100 a.insert(p,i,j) has the prerequisite "i and j are not iterators into a".

like image 182
ecatmur Avatar answered Oct 19 '22 13:10

ecatmur


Contrary to what a few other people have written here, I made the experience this week that this is not safe, at least when trying to have portable code with defined behavior.

Here is some example code that may expose undefined behavior:

std::vector<uint32_t> v;
v.push_back(0);
// some more push backs into v followed but are not shown here...

v.emplace_back(v.back()); // problem is here!

The above code ran on Linux with a g++ STL without problems.

When running the same code on Windows (compiled with Visual Studio 2013 Update5), the vector sometimes contained some garbled elements (seemingly random values).

The reason is that the reference returned by v.back() was invalidated due to the container reaching its capacity limit inside v.emplace_back(), before the element was added at the end.

I looked into VC++'s STL implementation of emplace_back() and it seemed to allocate new storage, copy over the existing vector elements into the new storage location, free the old storage and then construct the element at the end of the new storage. At that point, the referenced element's underlying memory may have been freed already or otherwise invalidated. That was producing undefined behavior, causing the vector elements inserted at reallocation thresholds to be garbled.

This seems to be a (still unfixed) bug in Visual Studio. With other STL implementations I tried, the problem did not occur.

In the end, you should avoid passing a reference to a vector element to the same vector's emplace_back() for now, at least if your code gets compiled with Visual Studio and is supposed to work.

like image 34
stj Avatar answered Oct 19 '22 12:10

stj


I checked my vector implementation and it works here as follows:

  1. Allocate new memory
  2. Emplace object
  3. Dealloc old memory

So everything is fine here. A similar implementation is used for push_back so this one is fine two.

FYI, here is the relevant part of the implementation. I have added comments:

template<typename _Tp, typename _Alloc>
    template<typename... _Args>
      void
      vector<_Tp, _Alloc>::
      _M_emplace_back_aux(_Args&&... __args)
      {
    const size_type __len =
      _M_check_len(size_type(1), "vector::_M_emplace_back_aux");
// HERE WE DO THE ALLOCATION
    pointer __new_start(this->_M_allocate(__len));
    pointer __new_finish(__new_start);
    __try
      {
// HERE WE EMPLACE THE ELEMENT
        _Alloc_traits::construct(this->_M_impl, __new_start + size(),
                     std::forward<_Args>(__args)...);
        __new_finish = 0;

        __new_finish
          = std::__uninitialized_move_if_noexcept_a
          (this->_M_impl._M_start, this->_M_impl._M_finish,
           __new_start, _M_get_Tp_allocator());

        ++__new_finish;
      }
    __catch(...)
      {
        if (!__new_finish)
          _Alloc_traits::destroy(this->_M_impl, __new_start + size());
        else
          std::_Destroy(__new_start, __new_finish, _M_get_Tp_allocator());
        _M_deallocate(__new_start, __len);
        __throw_exception_again;
      }
    std::_Destroy(this->_M_impl._M_start, this->_M_impl._M_finish,
              _M_get_Tp_allocator());
// HERE WE DESTROY THE OLD MEMORY
    _M_deallocate(this->_M_impl._M_start,
              this->_M_impl._M_end_of_storage
              - this->_M_impl._M_start);
    this->_M_impl._M_start = __new_start;
    this->_M_impl._M_finish = __new_finish;
    this->_M_impl._M_end_of_storage = __new_start + __len;
      }
like image 1
gexicide Avatar answered Oct 19 '22 12:10

gexicide