Actual initialization, when the array is being constructed
For actual initialization, not zeroing an existing array, an empty initializer list Just Works, zero-initializing an arbitrary sized array just like for plain int arr[N] = {};
.
See https://en.cppreference.com/w/cpp/language/zero_initialization - omitted elements in a braced initializer list are implicitly zero.
This also works in static storage, like with static
or at global scope.
#include <atomic>
#include <array>
#include <cstddef>
size_t foo(){
std::array< std::atomic<size_t>, 10 > arr {};
return arr[5].load(); // and use the array just to prove it works
}
// same asm as for a primitive array:
size_t bar(){
std::atomic_size_t arr[10] {};
return arr[5].load();
}
The initializer can be = {}
or {}
if you prefer, but ... arr[10] ()
makes it look like function-pointers.
See it on Godbolt - no warnings with -Wall -pedantic -std=c++11
or -std=c++23
from G++ 13.2, or clang++ 17 with or without -stdlib=libc++
. MSVC's -Wall
spews thousands of warnings from its standard library headers so it's unusable, but no errors from MSVC 19.37 or 19.14. Clang optimizes away the array init and load since it can see no other thread could have had a reference to the atomic objects; you can pass it to an opaque function to see clang's code-gen (as in the Godbolt link.)
I think this is truly standards-compliant and portable, not just happens-to-work on the big 3 mainstream compilers.
Non-initialization, zeroing after construction
For zeroing after the fact, like after some use but while we know no other threads could be reading or writing, memset
may be safe in practice, especially if it's lock-free and/or static_assert(sizeof(std::atomic<T>) == sizeof(T))
. That's definitely not standards-compliant, but it will work if the object-representation of std::atomic<T>
matches the object-representation of T
, which is how lock-free std::atomic
is implemented in practice.
Another option to make an array that's efficiently zeroable during single-threaded phases of a problem is to use atomic_ref
on an array of plain size_t
. But that means more code at every place that needs to do atomic accesses, and makes it possible to accidentally do a non-atomic access without getting a warning. Hopefully that's something clang -O2 -fsanitize=threads
can catch.
alignas( std::atomic_ref<size_t>::required_alignment ) std::array<size_t, 10> arr[10] = {};
Then std::fill
or memset
are both usable, as long as you don't create data-race UB by doing so while another thread is accessing the array (via atomic_ref
or otherwise).
See Efficient way to reset array of structs which contain a std::atomic member? for more details, including the fact that in asm on real machines, zeroing a whole array with aligned wide stores (e.g. 16-byte or 32-byte) atomically zeros each element, so you'd really like to take advantage of that if you would be looping over an array and assigning zero to each element. Because compilers won't optimize atomics, you'll get a separate 8-byte store for each element. (Which is not a disaster, but definitely worse for smaller types like atomic<uint8_t>
.) See Per-element atomicity of vector load/store and gather/scatter? - x86 manuals fail to guarantee this, although it's almost certainly true in practice on real hardware.