How to declare a vector of atomic in C++

34,428

Solution 1

As described in this closely related question that was mentioned in the comments, std::atomic<T> isn't copy-constructible, nor copy-assignable.

Object types that don't have these properties cannot be used as elements of std::vector.

However, it should be possible to create a wrapper around the std::atomic<T> element that is copy-constructible and copy-assignable. It will have to use the load() and store() member functions of std::atomic<T> to provide construction and assignment (this is the idea described by the accepted answer to the question mentioned above):

#include <atomic>
#include <vector>

template <typename T>
struct atomwrapper
{
  std::atomic<T> _a;

  atomwrapper()
    :_a()
  {}

  atomwrapper(const std::atomic<T> &a)
    :_a(a.load())
  {}

  atomwrapper(const atomwrapper &other)
    :_a(other._a.load())
  {}

  atomwrapper &operator=(const atomwrapper &other)
  {
    _a.store(other._a.load());
  }
};

int main(void)
{
  std::vector<atomwrapper<int>> v_a;
  std::atomic<int> a_i(1);
  v_a.push_back(a_i);
  return 0;
}

EDIT: As pointed out correctly by Bo Persson, the copy operation performed by the wrapper is not atomic. It enables you to copy atomic objects, but the copy itself isn't atomic. This means any concurrent access to the atomics must not make use of the copy operation. This implies that operations on the vector itself (e.g. adding or removing elements) must not be performed concurrently.

Example: If, say, one thread modifies the value stored in one of the atomics while another thread adds new elements to the vector, a vector reallocation may occur and the object the first thread modifies may be copied from one place in the vector to another. In that case there would be a data race between the element access performed by the first thread and the copy operation triggered by the second.

Solution 2

To first answer your headline question: you can declare a std::vector<std::atomic<...>> easily, as you have done in your example.

Because of the lack of copy or move constructors for std::atomic<> objects, however, your use of the vector will be restricted as you found out with the compilation error on push_back(). Basically you can't do anything that would invoke either constructor.

This means your vector's size must be fixed at construction, and you should manipulate elements using operator[] or .at(). For your example code, the following works1:

std::vector<std::atomic<int>> v_a(1);
std::atomic<int> a_i(1); 
v_a[0] = a_i.load();

If the "fixed size at construction" limitation is too onerous, you can use std::deque instead. This lets you emplace objects, growing the structure dynamically without requiring copy or move constructors, e.g.:

std::deque<std::atomic<int>> d;

d.emplace_back(1);
d.emplace_back(2);
d.pop_back();

There are still some limitations, however. For example, you can pop_back(), but you cannot use the more general erase(). The limitations make sense: an erase() in the middle of the blocks of contiguous storage used by std::deque in general requires the movement of elements, hence requires copy/move constructor or assignment operators to be present.

If you can't live with those limitations, you could create a wrapper class as suggested in other answers but be aware of the underlying implementation: it makes little sense to move a std::atomic<> object once it is being used: it would break any threads concurrently accessing the objects. The only sane use of copy/move constructors is generally in the initial setup of collections of these objects before they are published to other threads.


1 Unless, perhaps, you use Intel's icc compiler, which fails with an internal error while compiling this code.

Solution 3

Looks to me like atomic<T> has no copy constructor. Nor a move constructor, as far as I can tell.

One work around might be to use vector<T>::emplace_back() to construct the atomic in-place in the vector. Alas, I don't have a C++11 compiler on me right now, or I'd go and test it.

Share:
34,428
steffen
Author by

steffen

I am a Python freak and data nerd. Also, I am experimenting with creating videos on youtube. About Python and data. ¯(°_o)/¯ https://www.youtube.com/channel/UCG9XNnq9LodijOBpIVy1ILg

Updated on July 09, 2022

Comments

  • steffen
    steffen almost 2 years

    I am intending to declare a vector of atomic variables to be used as counters in a multithreaded programme. Here is what I tried:

    #include <atomic>
    #include <vector>
    
    int main(void)
    {
      std::vector<std::atomic<int>> v_a;
      std::atomic<int> a_i(1);
      v_a.push_back(a_i);
      return 0;
    }
    

    And this is the annoyingly verbose error message of gcc 4.6.3:

    In file included from /usr/include/c++/4.6/x86_64-linux-gnu/./bits/c++allocator.h:34:0,
                 from /usr/include/c++/4.6/bits/allocator.h:48,
                 from /usr/include/c++/4.6/vector:62,
                 from test_atomic_vec.h:2,
                 from test_atomic_vec.cc:1:
    /usr/include/c++/4.6/ext/new_allocator.h: In member function ‘void __gnu_cxx::new_allocator<_Tp>::construct(__gnu_cxx::new_allocator<_Tp>::pointer, const _Tp&) [with _Tp = std::atomic<int>, __gnu_cxx::new_allocator<_Tp>::pointer = std::atomic<int>*]’:
    /usr/include/c++/4.6/bits/stl_vector.h:830:6:   instantiated from ‘void std::vector<_Tp, _Alloc>::push_back(const value_type&) [with _Tp = std::atomic<int>, _Alloc = std::allocator<std::atomic<int> >, std::vector<_Tp, _Alloc>::value_type = std::atomic<int>]’
    test_atomic_vec.cc:10:20:   instantiated from here
    /usr/include/c++/4.6/ext/new_allocator.h:108:9: error: use of deleted function ‘std::atomic<int>::atomic(const std::atomic<int>&)’
    /usr/include/c++/4.6/atomic:538:7: error: declared here
    In file included from /usr/include/c++/4.6/vector:70:0,
                 from test_atomic_vec.h:2,
                 from test_atomic_vec.cc:1:
    /usr/include/c++/4.6/bits/vector.tcc: In member function ‘void std::vector<_Tp, _Alloc>::_M_insert_aux(std::vector<_Tp, _Alloc>::iterator, _Args&& ...) [with _Args = {const std::atomic<int>&}, _Tp = std::atomic<int>, _Alloc = std::allocator<std::atomic<int> >, std::vector<_Tp, _Alloc>::iterator = __gnu_cxx::__normal_iterator<std::atomic<int>*, std::vector<std::atomic<int> > >, typename std::_Vector_base<_Tp, _Alloc>::_Tp_alloc_type::pointer = std::atomic<int>*]’:
    /usr/include/c++/4.6/bits/stl_vector.h:834:4:   instantiated from ‘void std::vector<_Tp, _Alloc>::push_back(const value_type&) [with _Tp = std::atomic<int>, _Alloc = std::allocator<std::atomic<int> >, std::vector<_Tp, _Alloc>::value_type = std::atomic<int>]’
    test_atomic_vec.cc:10:20:   instantiated from here
    /usr/include/c++/4.6/bits/vector.tcc:319:4: error: use of deleted function ‘std::atomic<int>::atomic(const std::atomic<int>&)’
    /usr/include/c++/4.6/atomic:538:7: error: declared here
    /usr/include/c++/4.6/bits/stl_vector.h:834:4:   instantiated from ‘void std::vector<_Tp, _Alloc>::push_back(const value_type&) [with _Tp = std::atomic<int>, _Alloc = std::allocator<std::atomic<int> >, std::vector<_Tp, _Alloc>::value_type = std::atomic<int>]’
    test_atomic_vec.cc:10:20:   instantiated from here
    /usr/include/c++/4.6/bits/vector.tcc:319:4: error: use of deleted function ‘std::atomic<int>& std::atomic<int>::operator=(const std::atomic<int>&)’
    /usr/include/c++/4.6/atomic:539:15: error: declared here
    In file included from /usr/include/c++/4.6/x86_64-linux-gnu/./bits/c++allocator.h:34:0,
                 from /usr/include/c++/4.6/bits/allocator.h:48,
                 from /usr/include/c++/4.6/vector:62,
                 from test_atomic_vec.h:2,
                 from test_atomic_vec.cc:1:
    /usr/include/c++/4.6/ext/new_allocator.h: In member function ‘void __gnu_cxx::new_allocator<_Tp>::construct(__gnu_cxx::new_allocator<_Tp>::pointer, _Args&& ...) [with _Args = {std::atomic<int>}, _Tp = std::atomic<int>, __gnu_cxx::new_allocator<_Tp>::pointer = std::atomic<int>*]’:
    /usr/include/c++/4.6/bits/vector.tcc:306:4:   instantiated from ‘void std::vector<_Tp, _Alloc>::_M_insert_aux(std::vector<_Tp, _Alloc>::iterator, _Args&& ...) [with _Args = {const std::atomic<int>&}, _Tp = std::atomic<int>, _Alloc = std::allocator<std::atomic<int> >, std::vector<_Tp, _Alloc>::iterator = __gnu_cxx::__normal_iterator<std::atomic<int>*, std::vector<std::atomic<int> > >, typename std::_Vector_base<_Tp, _Alloc>::_Tp_alloc_type::pointer = std::atomic<int>*]’
    /usr/include/c++/4.6/bits/stl_vector.h:834:4:   instantiated from ‘void std::vector<_Tp, _Alloc>::push_back(const value_type&) [with _Tp = std::atomic<int>, _Alloc = std::allocator<std::atomic<int> >, std::vector<_Tp, _Alloc>::value_type = std::atomic<int>]’
    test_atomic_vec.cc:10:20:   instantiated from here
    /usr/include/c++/4.6/ext/new_allocator.h:114:4: error: use of deleted function ‘std::atomic<int>::atomic(const std::atomic<int>&)’
    /usr/include/c++/4.6/atomic:538:7: error: declared here
    In file included from /usr/include/c++/4.6/vector:61:0,
                 from test_atomic_vec.h:2,
                 from test_atomic_vec.cc:1:
    /usr/include/c++/4.6/bits/stl_algobase.h: In static member function ‘static _BI2 std::__copy_move_backward<true, false, std::random_access_iterator_tag>::__copy_move_b(_BI1, _BI1, _BI2) [with _BI1 = std::atomic<int>*, _BI2 = std::atomic<int>*]’:
    /usr/include/c++/4.6/bits/stl_algobase.h:581:18:   instantiated from ‘_BI2 std::__copy_move_backward_a(_BI1, _BI1, _BI2) [with bool _IsMove = true, _BI1 = std::atomic<int>*, _BI2 = std::atomic<int>*]’
    /usr/include/c++/4.6/bits/stl_algobase.h:590:34:   instantiated from ‘_BI2 std::__copy_move_backward_a2(_BI1, _BI1, _BI2) [with bool _IsMove = true, _BI1 = std::atomic<int>*, _BI2 = std::atomic<int>*]’
    /usr/include/c++/4.6/bits/stl_algobase.h:661:15:   instantiated from ‘_BI2 std::move_backward(_BI1, _BI1, _BI2) [with _BI1 = std::atomic<int>*, _BI2 = std::atomic<int>*]’
    /usr/include/c++/4.6/bits/vector.tcc:313:4:   instantiated from ‘void std::vector<_Tp, _Alloc>::_M_insert_aux(std::vector<_Tp, _Alloc>::iterator, _Args&& ...) [with _Args = {const std::atomic<int>&}, _Tp = std::atomic<int>, _Alloc = std::allocator<std::atomic<int> >, std::vector<_Tp, _Alloc>::iterator = __gnu_cxx::__normal_iterator<std::atomic<int>*, std::vector<std::atomic<int> > >, typename std::_Vector_base<_Tp, _Alloc>::_Tp_alloc_type::pointer = std::atomic<int>*]’
    /usr/include/c++/4.6/bits/stl_vector.h:834:4:   instantiated from ‘void std::vector<_Tp, _Alloc>::push_back(const value_type&) [with _Tp = std::atomic<int>, _Alloc = std::allocator<std::atomic<int> >, std::vector<_Tp, _Alloc>::value_type = std::atomic<int>]’
    test_atomic_vec.cc:10:20:   instantiated from here
    /usr/include/c++/4.6/bits/stl_algobase.h:546:6: error: use of deleted function ‘std::atomic<int>& std::atomic<int>::operator=(const std::atomic<int>&)’
    /usr/include/c++/4.6/atomic:539:15: error: declared here
    In file included from /usr/include/c++/4.6/vector:63:0,
                 from test_atomic_vec.h:2,
                 from test_atomic_vec.cc:1:
    /usr/include/c++/4.6/bits/stl_construct.h: In function ‘void std::_Construct(_T1*, _Args&& ...) [with _T1 = std::atomic<int>, _Args = {std::atomic<int>}]’:
    /usr/include/c++/4.6/bits/stl_uninitialized.h:77:3:   instantiated from ‘static _ForwardIterator std::__uninitialized_copy<_TrivialValueTypes>::__uninit_copy(_InputIterator, _InputIterator, _ForwardIterator) [with _InputIterator = std::move_iterator<std::atomic<int>*>, _ForwardIterator = std::atomic<int>*, bool _TrivialValueTypes = false]’
    /usr/include/c++/4.6/bits/stl_uninitialized.h:119:41:   instantiated from ‘_ForwardIterator std::uninitialized_copy(_InputIterator, _InputIterator, _ForwardIterator) [with _InputIterator = std::move_iterator<std::atomic<int>*>, _ForwardIterator = std::atomic<int>*]’
    /usr/include/c++/4.6/bits/stl_uninitialized.h:259:63:   instantiated from ‘_ForwardIterator std::__uninitialized_copy_a(_InputIterator, _InputIterator, _ForwardIterator, std::allocator<_Tp>&) [with _InputIterator = std::move_iterator<std::atomic<int>*>, _ForwardIterator = std::atomic<int>*, _Tp = std::atomic<int>]’
    /usr/include/c++/4.6/bits/stl_uninitialized.h:269:24:   instantiated from ‘_ForwardIterator std::__uninitialized_move_a(_InputIterator, _InputIterator, _ForwardIterator, _Allocator&) [with _InputIterator = std::atomic<int>*, _ForwardIterator = std::atomic<int>*, _Allocator = std::allocator<std::atomic<int> >]’
    /usr/include/c++/4.6/bits/vector.tcc:343:8:   instantiated from ‘void std::vector<_Tp, _Alloc>::_M_insert_aux(std::vector<_Tp, _Alloc>::iterator, _Args&& ...) [with _Args = {const std::atomic<int>&}, _Tp = std::atomic<int>, _Alloc = std::allocator<std::atomic<int> >, std::vector<_Tp, _Alloc>::iterator = __gnu_cxx::__normal_iterator<std::atomic<int>*, std::vector<std::atomic<int> > >, typename std::_Vector_base<_Tp, _Alloc>::_Tp_alloc_type::pointer = std::atomic<int>*]’
    /usr/include/c++/4.6/bits/stl_vector.h:834:4:   instantiated from ‘void std::vector<_Tp, _Alloc>::push_back(const value_type&) [with _Tp = std::atomic<int>, _Alloc = std::allocator<std::atomic<int> >, std::vector<_Tp, _Alloc>::value_type = std::atomic<int>]’
    test_atomic_vec.cc:10:20:   instantiated from here
    /usr/include/c++/4.6/bits/stl_construct.h:76:7: error: use of deleted function ‘std::atomic<int>::atomic(const std::atomic<int>&)’
    /usr/include/c++/4.6/atomic:538:7: error: declared here
    

    How do I solve this?

    The error disappears when I comment out the line with push_back() .

    edit: I edited the post... For those of you who saw the first post, the error was embarrassingly that I used gcc instead of g++ :\

  • steffen
    steffen over 11 years
    That seems to be the problem. putting v_a.push_back(std::move(a_i)); doesn't solve it though.
  • steffen
    steffen over 11 years
    that seems to do the job... Is there any downside to this solution? If not... why isn't this implemented in std::atomic in the first place?
  • Bo Persson
    Bo Persson over 11 years
    One problem is that _a.store(other._a.load()); doesn't look very atomic to me. Is this useful?
  • jogojapan
    jogojapan over 11 years
    @steffen The only downside I am aware of is that the implementation will have to take all necessary precautions to ensure all copies and assignments are performed atomically. This may involve memory fences and locks and therefore slow down the insertion of elements in the vector as well as reallocation and copy operations performed on the vector itself. But that is what it means to work with atomics. You can pass special memory model parameters as arguments to the load and store calls to improve things, but choosing the right memory model (other than the safe default one) is an art.
  • RobotMan
    RobotMan over 11 years
    @steffen You're right, atomic doesn't have a move constructor either. I have updated my answer with a work-around that is different to the accepted one.
  • jogojapan
    jogojapan over 11 years
    @BoPersson You are basically saying a mere assignment would perform the same operation as store, correct? Yes, I think that's right. Using store() explicitly helps emphasize that an atomic store operation happens there.
  • jogojapan
    jogojapan over 11 years
    I tried using emplace_back(1) instead of push_back(a_i), but GCC 7.2 rejects that, saying that the necessary unitialized-copy operation requires the copy constructor. I guess that is due to possible reallocations when a new element is inserted. Anyway, even if certain compilers accepted it, it would still be an incorrect use of std::vector, at least formally, because you are not supposed to use a non-copy-assignable element type.
  • Bo Persson
    Bo Persson over 11 years
    I see a problem with this not being one, but two, atomic operations. Just wonder if that is really useful and what the OP needs.
  • jogojapan
    jogojapan over 11 years
    @BoPersson Ah. That's right. For the OP: Indeed this implementation enables concurrent access to the elements of the vector, but not the vector itself. If, say, one thread modifies the value stored in one of the atomics while another thread adds new elements to the vector, a vector reallocation may occur and the object the first thread modifies may be copied from one place in the vector to another. In that case there would be a data race between the element access performed by the first thread and the copy operation triggered by the second.
  • steffen
    steffen over 11 years
    @BoPersson,@jogojapan: Good point. For the application I am working on right now it is ok, because I can separate initialisation and usage. But it's something to keep in mind.
  • betabandido
    betabandido over 11 years
    @jogojapan I just had a look at the C++ standard, and containers requirements are set on a per-operation basis (see section 23.2). That means, that there is no such a thing as an incorrect use of std::vector based on the element type. Of course, depending on the element type that you use, you will not be able to perform some operations. But if you do not need those operations, then using that element type should be totally fine.
  • jogojapan
    jogojapan over 11 years
    @betabandido Yes, strictly speaking that's right. The requirements are defined per operation, because that is how templates work in C++ -- only functions actually used are instantiated. However, it seems pretty clear that the idea is to have certain requirements on a per-class basis, and to actually enforce these using the concept of "concepts" (which unfortunately didn't make it into C++11). If you use GCC with the -D_GLIBCXX_CONCEPT_CHECKS option, the compiler will perform a limited set of per-class checks, and that did include the copy-assignment check for vector the last time I checked.
  • v.oddou
    v.oddou about 7 years
    @jogojapan I hope nobody assumes that a collection of concurrent objects is a concurrent collection of objects.
  • Peter Cordes
    Peter Cordes over 6 years
    Your constructors should take an order = seq_cst default arg, for use as the order for loads. I guess there's no way to get vector use it. Or maybe you should default to relaxed since this constructor is only intended for use while copying temporaries during construction or growth, and you can't resize / reserve / emplace_back on a vector that other threads have pointers into. Especially for the .store() in the copy-assignment operator. seq-cst stores are expensive. (And you can only do this while you hold a lock).
  • Peter Cordes
    Peter Cordes over 6 years
    Also make sure you don't do a[i] = b; and get one of these overloads. The OP's example is silly; copying from another std::atomic<int> a_i is really weird.
  • Peter Cordes
    Peter Cordes over 6 years
    What I mean is: you only need to support push_back / emplace_back(int) (or const T&) not atomic_int / const atomic<T> &. You should still force the user to explicitly load the value from some other atomic object. (And of course only one thread can grow the vector because the control block isn't atomic. But if you've reserved enough capacity to guarantee no reallocation, one thread can append while other threads are loading and storing earlier elements of the vector.)
  • BeeOnRope
    BeeOnRope over 6 years
    The core claim that Object types that don't have these properties cannot be used as elements of std::vector doesn't seem to be true, at least in C++11 and beyond. Objects without copy or move constructors can be used as long as you don't use operations on the vector that would require them, as described here. This doesn't mean you can't use push_back or emplace_back, but this isn't as restrictive as it sounds: moving an std::atomic<> object in memory pretty much breaks any concurrent accessors, so it's not a realistic operation.
  • Andy
    Andy over 6 years
    Comments are not for extended discussion; this conversation has been moved to chat.
  • Peter Cordes
    Peter Cordes over 6 years
    emplace_back doesn't work because it might have to grow the vector, and that requires copying the existing elements, requiring the non-existent copy-constructor for the atomic<T> elements. There's no "unsafe_emplace_back" that doesn't check capacity. You could define a wrapper for atomic that had a copy constructor, but probably just don't.
  • codetaku
    codetaku almost 6 years
    This is probably the best answer among the answers, but it's worth noting that you can use std::atomic as the vector type as long as you know how many elements you're going to have in advance. That is, if you know how many you'll have at the time you call the initializer, you'll be fine, such as vector<atomic<float>> vars(number_of_elements); to construct [number_of_elements] atomic floats which you can then use as you wish. It won't work with resize or push_back from there, though.
  • thefern
    thefern about 2 years
    this line v_a[0] = a_i; should be v_a[0] = a_i.load(); tried doing an edit but says suggestions are full.
  • BeeOnRope
    BeeOnRope about 2 years
    @thefern - good catch, I've fixed it up.
  • Ben Voigt
    Ben Voigt about 2 years
    Note to the modern reader: Use make_shared instead of the naked new operator found in the last code snippet.