Skip to content

[libc++] cv-qualified types in atomic and atomic_ref (P3323R1) #121414

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5afe474
[libc++] cv-qualified types in atomic_ref
dalg24 Dec 31, 2024
fc886c3
[libc++] check that std::atomic is only instantiated for cv-unqualifi…
dalg24 Dec 31, 2024
69d92fa
[libc++] Mark P3323R1 as complete
dalg24 Dec 31, 2024
9c6dd66
[libc++] Add testing for atomic_ref store/load for cv-qualified types
dalg24 Dec 31, 2024
1077d03
Trim trailing empty line in test to please clang-format
dalg24 Dec 31, 2024
28a0107
Add volatile T test for std::atomic
dalg24 Jan 2, 2025
26013d0
Fix GCC error about 2nd argument to the '__atomic' builtins being a p…
dalg24 Jan 3, 2025
ffdda72
Convert compare_exchange_strong test
dalg24 Jan 3, 2025
1b24aeb
Prefer parameter list in requires-expression to avoid declval
dalg24 Jan 7, 2025
a2f7780
Drop unnecessary std:: qualifiers
dalg24 Jan 8, 2025
9ec6b04
Fix constraint on specialization for pointer-to-object types
dalg24 Jan 9, 2025
d878901
Update tests for pointer types such as void*
dalg24 Jan 9, 2025
f350f7f
Undo cxx03 changes
dalg24 Jan 13, 2025
b156123
Merge branch 'main' of https://github.com/llvm/llvm-project into atom…
dalg24 Jan 13, 2025
e0fd1a2
Merge branch 'main' of https://github.com/llvm/llvm-project into atom…
dalg24 Feb 1, 2025
0b96b4c
Add a release note
dalg24 Feb 1, 2025
dcbee5b
Prefer not is_const and not is_volatile when checking cv-unqualified
dalg24 Feb 1, 2025
232a8a5
Prefer using-declaration for value_type in derived classes
dalg24 Feb 1, 2025
d84ec1f
Merge branch 'main' of https://github.com/llvm/llvm-project into atom…
dalg24 Mar 16, 2025
afe849f
Follow pattern suggested by Louis in the tests
dalg24 Mar 16, 2025
b8a4edc
Merge branch 'main' into atomic_ref_cv_qualified_types_p3323
ldionne Mar 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion libcxx/docs/Status/Cxx2cPapers.csv
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
"`P3050R2 <https://wg21.link/P3050R2>`__","Fix C++26 by optimizing ``linalg::conjugated`` for noncomplex value types","2024-11 (Wrocław)","","",""
"`P3396R1 <https://wg21.link/P3396R1>`__","``std::execution`` wording fixes","2024-11 (Wrocław)","","",""
"`P2835R7 <https://wg21.link/P2835R7>`__","Expose ``std::atomic_ref``'s object address","2024-11 (Wrocław)","","",""
"`P3323R1 <https://wg21.link/P3323R1>`__","cv-qualified types in ``atomic`` and ``atomic_ref``","2024-11 (Wrocław)","","",""
"`P3323R1 <https://wg21.link/P3323R1>`__","cv-qualified types in ``atomic`` and ``atomic_ref``","2024-11 (Wrocław)","|Complete|","20","Implemented as DR against C++20."
"`P3508R0 <https://wg21.link/P3508R0>`__","Wording for ""constexpr for specialized memory algorithms""","2024-11 (Wrocław)","","",""
"`P3369R0 <https://wg21.link/P3369R0>`__","constexpr for ``uninitialized_default_construct``","2024-11 (Wrocław)","","",""
"`P3370R1 <https://wg21.link/P3370R1>`__","Add new library headers from C23","2024-11 (Wrocław)","","",""
Expand Down
276 changes: 212 additions & 64 deletions libcxx/include/__atomic/atomic_ref.h

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions libcxx/include/__atomic/support.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
#define _LIBCPP___ATOMIC_SUPPORT_H

#include <__config>
#include <__type_traits/is_same.h>
#include <__type_traits/is_trivially_copyable.h>
#include <__type_traits/remove_cv.h>

#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
# pragma GCC system_header
Expand Down Expand Up @@ -114,6 +116,8 @@ _LIBCPP_BEGIN_NAMESPACE_STD
template <typename _Tp, typename _Base = __cxx_atomic_base_impl<_Tp> >
struct __cxx_atomic_impl : public _Base {
static_assert(is_trivially_copyable<_Tp>::value, "std::atomic<T> requires that 'T' be a trivially copyable type");
static_assert(is_same<_Tp, typename remove_cv<_Tp>::type>::value,
"std::atomic<T> requires that 'T' be a cv-unqualified type");

_LIBCPP_HIDE_FROM_ABI __cxx_atomic_impl() _NOEXCEPT = default;
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR explicit __cxx_atomic_impl(_Tp __value) _NOEXCEPT : _Base(__value) {}
Expand Down
4 changes: 4 additions & 0 deletions libcxx/include/__cxx03/__atomic/cxx_atomic_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
#include <__cxx03/__config>
#include <__cxx03/__memory/addressof.h>
#include <__cxx03/__type_traits/is_assignable.h>
#include <__cxx03/__type_traits/is_same.h>
#include <__cxx03/__type_traits/is_trivially_copyable.h>
#include <__cxx03/__type_traits/remove_const.h>
#include <__cxx03/__type_traits/remove_cv.h>
#include <__cxx03/cstddef>

#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
Expand Down Expand Up @@ -500,6 +502,8 @@ __cxx_atomic_fetch_xor(__cxx_atomic_base_impl<_Tp>* __a, _Tp __pattern, memory_o
template <typename _Tp, typename _Base = __cxx_atomic_base_impl<_Tp> >
struct __cxx_atomic_impl : public _Base {
static_assert(is_trivially_copyable<_Tp>::value, "std::atomic<T> requires that 'T' be a trivially copyable type");
static_assert(is_same<_Tp, typename remove_cv<_Tp>::type>::value,
"std::atomic<T> requires that 'T' be a cv-unqualified type");

_LIBCPP_HIDE_FROM_ABI __cxx_atomic_impl() _NOEXCEPT = default;
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR explicit __cxx_atomic_impl(_Tp __value) _NOEXCEPT : _Base(__value) {}
Expand Down
42 changes: 29 additions & 13 deletions libcxx/test/std/atomics/atomics.ref/assign.pass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,42 @@
#include "test_helper.h"
#include "test_macros.h"

template <typename T>
template <typename U>
struct TestAssign {
void operator()() const {
{
T x(T(1));
std::atomic_ref<T> const a(x);
static_assert(std::is_nothrow_assignable_v<std::atomic_ref<U>, U>);
do_test<U>();
do_test_atomic<U>();
static_assert(!std::is_assignable_v<std::atomic_ref<U const>, U>);
if constexpr (std::atomic_ref<U>::is_always_lock_free) {
static_assert(std::is_nothrow_assignable_v<std::atomic_ref<U volatile>, U>);
do_test<U volatile>();
do_test_atomic<U volatile>();
static_assert(!std::is_assignable_v<std::atomic_ref<U const volatile>, U>);
}
}

std::same_as<T> decltype(auto) y = (a = T(2));
assert(y == T(2));
assert(x == T(2));
template <typename T>
void do_test() const {
T x(T(1));
std::atomic_ref<T> const a(x);

ASSERT_NOEXCEPT(a = T(0));
static_assert(std::is_nothrow_assignable_v<std::atomic_ref<T>, T>);
std::same_as<std::remove_cv_t<T>> decltype(auto) y = (a = T(2));
assert(y == std::remove_cv_t<T>(2));
assert(const_cast<std::remove_cv_t<T> const&>(x) == std::remove_cv_t<T>(2));

static_assert(!std::is_copy_assignable_v<std::atomic_ref<T>>);
}
ASSERT_NOEXCEPT(a = T(0));

static_assert(!std::is_copy_assignable_v<std::atomic_ref<T>>);
}

template <typename T>
void do_test_atomic() const {
{
auto assign = [](std::atomic_ref<T> const& y, T, T new_val) { y = new_val; };
auto load = [](std::atomic_ref<T> const& y) { return y.load(); };
auto assign = [](std::atomic_ref<T> const& y, T const&, T const& new_val) {
y = const_cast<std::remove_cv_t<T> const&>(new_val);
};
auto load = [](std::atomic_ref<T> const& y) { return y.load(); };
test_seq_cst<T>(assign, load);
}
}
Expand Down
34 changes: 27 additions & 7 deletions libcxx/test/std/atomics/atomics.ref/convert.pass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,42 @@
#include "test_helper.h"
#include "test_macros.h"

template <typename T>
template <typename U>
struct TestConvert {
void operator()() const {
static_assert(std::is_nothrow_convertible_v<std::atomic_ref<U>, U>);
do_test<U>();
do_test_atomic<U>();
static_assert(std::is_nothrow_convertible_v<std::atomic_ref<U const>, U>);
do_test<U const>();
if constexpr (std::atomic_ref<U>::is_always_lock_free) {
static_assert(std::is_nothrow_convertible_v<std::atomic_ref<U volatile>, U>);
do_test<U volatile>();
do_test_atomic<U volatile>();
static_assert(std::is_nothrow_convertible_v<std::atomic_ref<U const volatile>, U>);
do_test<U const volatile>();
}
}

template <class T>
void do_test() const {
T x(T(1));

T copy = x;
T copy = const_cast<std::remove_cv_t<T> const&>(x);
std::atomic_ref<T> const a(copy);

T converted = a;
assert(converted == x);
std::remove_cv_t<T> converted = a;
assert(converted == const_cast<std::remove_cv_t<T> const&>(x));

ASSERT_NOEXCEPT(T(a));
static_assert(std::is_nothrow_convertible_v<std::atomic_ref<T>, T>);
}

auto store = [](std::atomic_ref<T> const& y, T, T new_val) { y.store(new_val); };
auto load = [](std::atomic_ref<T> const& y) { return static_cast<T>(y); };
template <class T>
void do_test_atomic() const {
auto store = [](std::atomic_ref<T> const& y, T const&, T const& new_val) {
y.store(const_cast<std::remove_cv_t<T> const&>(new_val));
};
auto load = [](std::atomic_ref<T> const& y) { return static_cast<std::remove_cv_t<T>>(y); };
test_seq_cst<T>(store, load);
}
};
Expand Down
13 changes: 11 additions & 2 deletions libcxx/test/std/atomics/atomics.ref/deduction.pass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,18 @@
template <typename T>
struct TestDeduction {
void operator()() const {
T x(T(0));
do_test<T>();
do_test<T const>();
if constexpr (std::atomic_ref<T>::is_always_lock_free) {
do_test<T volatile>();
do_test<T const volatile>();
}
}
template <class U>
void do_test() const {
U x(U(0));
std::atomic_ref a(x);
ASSERT_SAME_TYPE(decltype(a), std::atomic_ref<T>);
ASSERT_SAME_TYPE(decltype(a), std::atomic_ref<U>);
}
};

Expand Down
35 changes: 27 additions & 8 deletions libcxx/test/std/atomics/atomics.ref/load.pass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,44 @@
#include "test_helper.h"
#include "test_macros.h"

template <typename T>
template <typename U>
struct TestLoad {
void operator()() const {
do_test<U>();
do_test_atomic<U>();
do_test<U const>();
if constexpr (std::atomic_ref<U>::is_always_lock_free) {
do_test<U volatile>();
do_test_atomic<U volatile>();
do_test<U const volatile>();
}
}

template <class T>
void do_test() const {
T x(T(1));
std::atomic_ref<T> const a(x);

{
std::same_as<T> decltype(auto) y = a.load();
assert(y == T(1));
std::same_as<std::remove_cv_t<T>> decltype(auto) y = a.load();
assert(y == std::remove_cv_t<T>(1));
ASSERT_NOEXCEPT(a.load());
}

{
std::same_as<T> decltype(auto) y = a.load(std::memory_order_seq_cst);
assert(y == T(1));
std::same_as<std::remove_cv_t<T>> decltype(auto) y = a.load(std::memory_order_seq_cst);
assert(y == std::remove_cv_t<T>(1));
ASSERT_NOEXCEPT(a.load(std::memory_order_seq_cst));
}
}

template <class T>
void do_test_atomic() const {
// memory_order::seq_cst
{
auto store = [](std::atomic_ref<T> const& y, T, T new_val) { y.store(new_val); };
auto store = [](std::atomic_ref<T> const& y, T const&, T const& new_val) {
y.store(const_cast<std::remove_cv_t<T> const&>(new_val));
};
auto load_no_arg = [](std::atomic_ref<T> const& y) { return y.load(); };
auto load_with_order = [](std::atomic_ref<T> const& y) { return y.load(std::memory_order::seq_cst); };
test_seq_cst<T>(store, load_no_arg);
Expand All @@ -49,8 +66,10 @@ struct TestLoad {

// memory_order::release
{
auto store = [](std::atomic_ref<T> const& y, T, T new_val) { y.store(new_val, std::memory_order::release); };
auto load = [](std::atomic_ref<T> const& y) { return y.load(std::memory_order::acquire); };
auto store = [](std::atomic_ref<T> const& y, T const&, T const& new_val) {
y.store(const_cast<std::remove_cv_t<T> const&>(new_val), std::memory_order::release);
};
auto load = [](std::atomic_ref<T> const& y) { return y.load(std::memory_order::acquire); };
test_acquire_release<T>(store, load);
}
}
Expand Down
40 changes: 32 additions & 8 deletions libcxx/test/std/atomics/atomics.ref/store.pass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,49 @@
#include "test_helper.h"
#include "test_macros.h"

template <typename T>
template <typename T, typename U>
concept has_store = requires { std::declval<T>().store(std::declval<U>()); };

template <typename U>
struct TestStore {
void operator()() const {
static_assert(has_store<std::atomic_ref<U>, U>);
do_test<U>();
do_test_atomic<U>();
static_assert(!has_store<std::atomic_ref<U const>, U>);
if constexpr (std::atomic_ref<U>::is_always_lock_free) {
static_assert(has_store<std::atomic_ref<U volatile>, U>);
do_test<U volatile>();
do_test_atomic<U volatile>();
static_assert(!has_store<std::atomic_ref<U const volatile>, U>);
}
}

template <typename T>
void do_test() const {
T x(T(1));
std::atomic_ref<T> const a(x);

a.store(T(2));
assert(x == T(2));
assert(const_cast<std::remove_cv_t<T> const&>(x) == std::remove_cv_t<T>(2));
ASSERT_NOEXCEPT(a.store(T(1)));

a.store(T(3), std::memory_order_seq_cst);
assert(x == T(3));
assert(const_cast<std::remove_cv_t<T> const&>(x) == std::remove_cv_t<T>(3));
ASSERT_NOEXCEPT(a.store(T(0), std::memory_order_seq_cst));
}

template <typename T>
void do_test_atomic() const {
// TODO memory_order::relaxed

// memory_order::seq_cst
{
auto store_no_arg = [](std::atomic_ref<T> const& y, T, T new_val) { y.store(new_val); };
auto store_with_order = [](std::atomic_ref<T> const& y, T, T new_val) {
y.store(new_val, std::memory_order::seq_cst);
auto store_no_arg = [](std::atomic_ref<T> const& y, T const&, T const& new_val) {
y.store(const_cast<std::remove_cv_t<T> const&>(new_val));
};
auto store_with_order = [](std::atomic_ref<T> const& y, T const&, T const& new_val) {
y.store(const_cast<std::remove_cv_t<T> const&>(new_val), std::memory_order::seq_cst);
};
auto load = [](std::atomic_ref<T> const& y) { return y.load(); };
test_seq_cst<T>(store_no_arg, load);
Expand All @@ -48,8 +70,10 @@ struct TestStore {

// memory_order::release
{
auto store = [](std::atomic_ref<T> const& y, T, T new_val) { y.store(new_val, std::memory_order::release); };
auto load = [](std::atomic_ref<T> const& y) { return y.load(std::memory_order::acquire); };
auto store = [](std::atomic_ref<T> const& y, T const&, T const& new_val) {
y.store(const_cast<std::remove_cv_t<T> const&>(new_val), std::memory_order::release);
};
auto load = [](std::atomic_ref<T> const& y) { return y.load(std::memory_order::acquire); };
test_acquire_release<T>(store, load);
}
}
Expand Down
39 changes: 20 additions & 19 deletions libcxx/test/std/atomics/atomics.ref/test_helper.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ template <class T, class StoreOp, class LoadOp>
void test_seq_cst(StoreOp store_op, LoadOp load_op) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we introduce using Unqualified = std::remove_cv_t<T> here like we do in other places?

#ifndef TEST_HAS_NO_THREADS
for (int i = 0; i < 100; ++i) {
T old_value(make_value<T>(0));
T new_value(make_value<T>(1));
T old_value(make_value<std::remove_cv_t<T>>(0));
T new_value(make_value<std::remove_cv_t<T>>(1));
Comment on lines +48 to +49
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
T old_value(make_value<std::remove_cv_t<T>>(0));
T new_value(make_value<std::remove_cv_t<T>>(1));
Unqualified old_value(make_value<Unqualified>(0));
Unqualified new_value(make_value<Unqualified>(1));


T copy_x = old_value;
T copy_x = const_cast<std::remove_cv_t<T> const&>(old_value);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why this needs a const_cast, can you explain?

std::atomic_ref<T> const x(copy_x);
T copy_y = old_value;
T copy_y = const_cast<std::remove_cv_t<T> const&>(old_value);
std::atomic_ref<T> const y(copy_y);

std::atomic_bool x_updated_first(false);
Expand All @@ -61,19 +61,19 @@ void test_seq_cst(StoreOp store_op, LoadOp load_op) {
auto t2 = support::make_test_thread([&] { store_op(y, old_value, new_value); });

auto t3 = support::make_test_thread([&] {
while (!equals(load_op(x), new_value)) {
while (!equals(load_op(x), const_cast<std::remove_cv_t<T> const&>(new_value))) {
std::this_thread::yield();
}
if (!equals(load_op(y), new_value)) {
if (!equals(load_op(y), const_cast<std::remove_cv_t<T> const&>(new_value))) {
x_updated_first.store(true, std::memory_order_relaxed);
}
});

auto t4 = support::make_test_thread([&] {
while (!equals(load_op(y), new_value)) {
while (!equals(load_op(y), const_cast<std::remove_cv_t<T> const&>(new_value))) {
std::this_thread::yield();
}
if (!equals(load_op(x), new_value)) {
if (!equals(load_op(x), const_cast<std::remove_cv_t<T> const&>(new_value))) {
y_updated_first.store(true, std::memory_order_relaxed);
}
});
Expand All @@ -98,10 +98,10 @@ template <class T, class StoreOp, class LoadOp>
void test_acquire_release(StoreOp store_op, LoadOp load_op) {
#ifndef TEST_HAS_NO_THREADS
for (auto i = 0; i < 100; ++i) {
T old_value(make_value<T>(0));
T new_value(make_value<T>(1));
T old_value(make_value<std::remove_cv_t<T>>(0));
T new_value(make_value<std::remove_cv_t<T>>(1));

T copy = old_value;
T copy = const_cast<std::remove_cv_t<T> const&>(old_value);
std::atomic_ref<T> const at(copy);
int non_atomic = 5;

Expand All @@ -110,14 +110,15 @@ void test_acquire_release(StoreOp store_op, LoadOp load_op) {
threads.reserve(number_of_threads);

for (auto j = 0; j < number_of_threads; ++j) {
threads.push_back(support::make_test_thread([&at, &non_atomic, load_op, new_value] {
while (!equals(load_op(at), new_value)) {
std::this_thread::yield();
}
// Other thread's writes before the release store are visible
// in this thread's read after the acquire load
assert(non_atomic == 6);
}));
threads.push_back(support::make_test_thread(
[&at, &non_atomic, load_op, new_value = const_cast<std::remove_cv_t<T> const&>(new_value)] {
while (!equals(load_op(at), new_value)) {
std::this_thread::yield();
}
// Other thread's writes before the release store are visible
// in this thread's read after the acquire load
assert(non_atomic == 6);
}));
}

non_atomic = 6;
Expand Down
Loading
Loading