Atomics receives a few essential extensions in C++20. Today, I start with the new data type std::atomic_ref.

The type std::atomic_ref
applies atomic operations to its referenced object.
std::atomic_ref
Concurrent writing and reading using a std::atomic_ref
is no data race. The lifetime of the referenced object must exceed the lifetime of the std::atomic_ref
. Accessing a subobject of the referenced object with a std::atomic_ref
is not well-defined.
Motivation
You may think using a reference inside an atomic would do the job. Unfortunately not.
In the following program, I have a class ExpensiveToCopy
, which includes a counter
. The counter
is concurrently incremented by a few threads. Consequently, counter
has to be protected.
// atomicReference.cpp
#include <atomic>
#include <iostream>
#include <random>
#include <thread>
#include <vector>
struct ExpensiveToCopy {
int counter{};
};
int getRandom(int begin, int end) { // (6)
std::random_device seed; // initial seed
std::mt19937 engine(seed()); // generator
std::uniform_int_distribution<> uniformDist(begin, end);
return uniformDist(engine);
}
void count(ExpensiveToCopy& exp) { // (2)
std::vector<std::thread> v;
std::atomic<int> counter{exp.counter}; // (3)
for (int n = 0; n < 10; ++n) { // (4)
v.emplace_back([&counter] {
auto randomNumber = getRandom(100, 200); // (5)
for (int i = 0; i < randomNumber; ++i) { ++counter; }
});
}
for (auto& t : v) t.join();
}
int main() {
std::cout << std::endl;
ExpensiveToCopy exp; // (1)
count(exp);
std::cout << "exp.counter: " << exp.counter << '\n';
std::cout << std::endl;
}
exp
(1) is the expensive-to-copy object. For performance reasons, the function count
(2) takes exp
by reference. count
initializes the std::atomic<int>
with exp.counter (
3). The following lines create ten threads (4), each performing the lambda expression, which takes counter
by reference. The lambda expression gets a random number between 100 and 200 (5) and increments the counter exactly as often. The function getRandom
(6) start with an initial seed and create a uniformly distributed number via the random number generator Mersenne Twister.
In the end, the exp.counter
(7) should have an approximate value of 1500 because the ten threads increment on average 150 times. Executing the program on the Wandbox online compiler gave me a surprising result.

The counter is 0. What is happening? The issue is in line (3). The initialization in the expression std::atomic<int> counter{exp.counter}
creates a copy. The following small program exemplifies the issue.
// atomicRefCopy.cpp
#include <atomic>
#include <iostream>
int main() {
std::cout << std::endl;
int val{5};
int& ref = val; // (2)
std::atomic<int> atomicRef(ref);
++atomicRef; // (1)
std::cout << "ref: " << ref << std::endl;
std::cout << "atomicRef.load(): " << atomicRef.load() << std::endl;
std::cout << std::endl;
}
The increment operation (1) does not address the reference ref
(2). The value of ref
is not changed.

Replacing the std::atomic<int> counter{exp.counter}
with std::atomic_ref<int> counter{exp.counter
} solves the issue:
// atomicReference.cpp
#include <atomic>
#include <iostream>
#include <random>
#include <thread>
#include <vector>
struct ExpensiveToCopy {
int counter{};
};
int getRandom(int begin, int end) {
std::random_device seed; // initial randomness
std::mt19937 engine(seed()); // generator
std::uniform_int_distribution<> uniformDist(begin, end);
return uniformDist(engine);
}
void count(ExpensiveToCopy& exp) {
std::vector<std::thread> v;
std::atomic_ref<int> counter{exp.counter};
for (int n = 0; n < 10; ++n) {
v.emplace_back([&counter] {
auto randomNumber = getRandom(100, 200);
for (int i = 0; i < randomNumber; ++i) { ++counter; }
});
}
for (auto& t : v) t.join();
}
int main() {
std::cout << std::endl;
ExpensiveToCopy exp;
count(exp);
std::cout << "exp.counter: " << exp.counter << '\n';
std::cout << std::endl;
}
Now, the value of counter
is as expected:

To be Atomic or Not to be Atomic
You may ask me why I didn't make the counter atomic in the first place:
struct ExpensiveToCopy {
std::atomic<int> counter{};
};
Of course, this is a valid approach, but this approach has a significant downside. Each access to the counter is synchronized, and synchronization is not for free. On the contrary, using a std::atomic_ref<int> counter
lets you explicitly control when you need atomic access to the counter. Most of the time, you may only want to read the value of the counter. Consequently, defining it as an atomic is pessimization.
Let me conclude my post with a few more details about the class template std::atomic_ref
.
Modernes C++ Mentoring
Be part of my mentoring programs:
Do you want to stay informed about my mentoring programs: Subscribe via E-Mail.
Specializations of std::atomic_ref
You can specialize std::atomic_ref
for user-defined types, use partial specializations for pointer types or full specializations for arithmetic types such as integral or floating-point types.
Primary Template
The primary template std::atomic_ref
can be instantiated with a trivially copyable type T. Trivially copyable types are either scalar types (arithmetic types, enum'
s, pointers, member pointers, or std::nullptr_t
's), or trivially copyable classes and arrays of scalar types
Partial Specializations for Pointer Types
The standard provides partial specializations for a pointer type: std::atomic_ref<t*>
.
Specializations for Arithmetic Types
The standard provides specialization for the integral and floating-point types: std::atomic_ref<arithmetic type>
.
- Character types:
char, char8_t
(C++20), char16_t, char32_t, and wchar_t
- Standard signed integer types:
signed char, short, int, long,
and long long
- Standard unsigned integer types:
unsigned char, unsigned short, unsigned int, unsigned long
, and unsigned long long
- Additional integer types, defined in the header
<cstdint>
- Standard floating-point types:
float
, double
, and long double
All Atomic Operations
First, here is the list of all operations on std::atomic_ref
.

The composite assignment operators (+=, -=, |=, &=
, or ^=
) return the new value; the fetch
variations return the old value. The compare_exchange_strong
and compare_exchange_weak
perform an atomic exchange
if equal and an atomic load
if not. They return true
in the success case, otherwise false
. Each function supports an additional memory-ordering argument. The default is sequential consistency.
Of course, not all operations are available on all types referenced by std::atomic_ref.
The table shows the list of all atomic operations depending on the type referenced by std::atomic_ref
.

When you study the last two tables carefully, you notice that you can use std::atomic_ref
to synchronize threads.
What's next?
std::atomic
and std::atomic_ref
support in C++20 member functions notify_one
, notify_all
, and wait.
The three functions provide a convenient way to synchronize threads. In my next post, I will have a closer look at std::atomic
and, in particular, the thread synchronization with std::atomic
's
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Matthieu Bolt, Stephen Kelley, Kyle Dean, Tusar Palauri, Dmitry Farberov, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, and Rob North.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, and Slavko Radman.
My special thanks to Embarcadero 
My special thanks to PVS-Studio 
My special thanks to Tipi.build 
My special thanks to Take Up code 
Seminars
I'm happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.
Bookable (Online)
German
Standard Seminars (English/German)
Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.
- C++ - The Core Language
- C++ - The Standard Library
- C++ - Compact
- C++11 and C++14
- Concurrency with Modern C++
- Design Pattern and Architectural Pattern with C++
- Embedded Programming with Modern C++
- Generic Programming (Templates) with C++
New
- Clean Code with Modern C++
- C++20
Contact Me
- Phone: +49 7472 917441
- Mobil:: +49 176 5506 5086
- Mail: This email address is being protected from spambots. You need JavaScript enabled to view it.
- German Seminar Page: www.ModernesCpp.de
- Mentoring Page: www.ModernesCpp.org
Modernes C++,

Comments
When you don't need atomicity just use an int.
The performance difference of an atomic using sequenial-consistency and relaxed ordering is on x86 probably minimal. This may change on other architectures.
On the end, you have to measure.
RSS feed for comments to this post