Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ endif()

add_versioned_package("gh:boostorg/mp11#boost-1.83.0")
fmt_recipe(10.2.1)
add_versioned_package("gh:intel/cpp-baremetal-concurrency#27de8e1")
add_versioned_package("gh:intel/cpp-baremetal-concurrency#06e5901")

if(NOT DEFINED CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 20)
Expand All @@ -39,6 +39,7 @@ target_sources(
include
FILES
include/stdx/algorithm.hpp
include/stdx/atomic.hpp
include/stdx/atomic_bitset.hpp
include/stdx/bit.hpp
include/stdx/bitset.hpp
Expand Down
49 changes: 49 additions & 0 deletions docs/atomic.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@

== `atomic.hpp`

https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/atomic.hpp[`atomic.hpp`]
provides an implementation of
https://en.cppreference.com/w/cpp/atomic/atomic[`std::atomic`] with a few
differences.

`stdx::atomic` does not implement:

* `is_lock_free` or `is_always_lock_free`
* `compare_exchange_{weak,strong}`
* `wait`
* `notify_{one,all}`
* `fetch_{max,min}`

However, `stdx::atomic` allows customization of the atomic implementation for
best codegen. `stdx::atomic` is implemented using the atomic API exposed by
Intel's https://github.com/intel/cpp-baremetal-concurrency[baremetal concurrency
library].

For example, it is possible that a particular platform requires atomic accesses
to be 32-bit aligned. To achieve that for `stdx::atomic<bool>`, we could provide a
configuration header specializing `::atomic::alignment_of`:

[source,cpp]
----
// this header: atomic_cfg.hpp
#include <cstdint>

template <>
constexpr inline auto ::atomic::alignment_of<bool> = alignof(std::uint32_t);
----

To apply this configuration, when compiling, pass `-DATOMIC_CFG="<path>/atomic_cfg.hpp"`.
The result would be that `stdx::atomic<bool>` has 32-bit alignment:

[source,cpp]
----
static_assert(alignof(stdx::atomic<bool>) == alignof(std::uint32_t));
----

Using the https://github.com/intel/cpp-baremetal-concurrency[baremetal
concurrency library] it is possible to override the handling of atomic access
(`load`, `store`, `exchange`, `fetch_<op>`) to ensure the best codegen on a
particular platform. As well as alignment concerns, for instance it may be the
case on a single-core microcontroller that it is cheaper to disable and
re-enable interrupts around a read/write than incurring a lock-free atomic
access.
1 change: 1 addition & 0 deletions docs/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
:toc: left

include::intro.adoc[]
include::atomic.adoc[]
include::atomic_bitset.adoc[]
include::algorithm.adoc[]
include::bit.adoc[]
Expand Down
1 change: 1 addition & 0 deletions docs/intro.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ into headers whose names match the standard.
The following headers are available:

* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/algorithm.hpp[`algorithm.hpp`]
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/atomic.hpp[`atomic.hpp`]
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/atomic_bitset.hpp[`atomic_bitset.hpp`]
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/bit.hpp[`bit.hpp`]
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/bitset.hpp[`bitset.hpp`]
Expand Down
122 changes: 122 additions & 0 deletions include/stdx/atomic.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#pragma once

#include <conc/atomic.hpp>

#include <atomic>
#include <type_traits>

#if __cplusplus >= 202002L
#define CPP20(...) __VA_ARGS__
#else
#define CPP20(...)
#endif

namespace stdx {
inline namespace v1 {
template <typename T> class atomic {
static_assert(std::is_trivially_copyable_v<T> and
std::is_copy_constructible_v<T> and
std::is_move_constructible_v<T> and
std::is_copy_assignable_v<T> and
std::is_move_assignable_v<T>,
"Atomic values must be trivially copyable, copy "
"constructible and copy assignable");

using elem_t = ::atomic::atomic_type_t<T>;
constexpr static auto alignment = ::atomic::alignment_of<T>;

static_assert(std::is_convertible_v<elem_t, T>,
"::atomic::atomic_type_t specialization result must be "
"convertible to T");
static_assert(std::is_convertible_v<T, elem_t>,
"::atomic::atomic_type_t specialization result must be "
"convertible from T");

alignas(alignment) elem_t value;

public:
using value_type = T;

constexpr atomic() CPP20(requires std::is_default_constructible_v<elem_t>)
: value{} {}
constexpr atomic(T t) : value{static_cast<elem_t>(t)} {}
atomic(atomic const &) = delete;
auto operator=(atomic const &) -> atomic & = delete;

[[nodiscard]] auto
load(std::memory_order mo = std::memory_order_seq_cst) const -> T {
return static_cast<T>(::atomic::load(value, mo));
}

void store(T t, std::memory_order mo = std::memory_order_seq_cst) {
::atomic::store(value, static_cast<elem_t>(t), mo);
}

[[nodiscard]] operator T() const { return load(); }
auto operator=(T t) -> T {
store(t);
return t;
}

[[nodiscard]] auto
exchange(T t, std::memory_order mo = std::memory_order_seq_cst) -> T {
return ::atomic::exchange(value, static_cast<elem_t>(t), mo);
}

auto fetch_add(T t, std::memory_order mo = std::memory_order_seq_cst) -> T {
CPP20(static_assert(
requires { t + t; }, "T must support operator+(x, y)"));
return ::atomic::fetch_add(value, static_cast<elem_t>(t), mo);
}
auto fetch_sub(T t, std::memory_order mo = std::memory_order_seq_cst) -> T {
CPP20(static_assert(
requires { t - t; }, "T must support operator-(x, y)"));
return ::atomic::fetch_sub(value, static_cast<elem_t>(t), mo);
}

auto operator+=(T t) -> T { return fetch_add(t) + t; }
auto operator-=(T t) -> T { return fetch_sub(t) - t; }

auto operator++() -> T {
CPP20(static_assert(
requires(T t) { ++t; }, "T must support operator++()"));
return ::atomic::fetch_add(value, 1) + 1;
}
[[nodiscard]] auto operator++(int) -> T {
CPP20(static_assert(
requires(T t) { t++; }, "T must support operator++(int)"));
return ::atomic::fetch_add(value, 1);
}
auto operator--() -> T {
CPP20(static_assert(
requires(T t) { --t; }, "T must support operator--()"));
return ::atomic::fetch_sub(value, 1) - 1;
}
[[nodiscard]] auto operator--(int) -> T {
CPP20(static_assert(
requires(T t) { t--; }, "T must support operator--(int)"));
return ::atomic::fetch_sub(value, 1);
}

auto fetch_and(T t, std::memory_order mo = std::memory_order_seq_cst) -> T {
CPP20(static_assert(
requires { t & t; }, "T must support operator&(x, y)"));
return ::atomic::fetch_and(value, static_cast<elem_t>(t), mo);
}
auto fetch_or(T t, std::memory_order mo = std::memory_order_seq_cst) -> T {
CPP20(static_assert(
requires { t | t; }, "T must support operator|(x, y)"));
return ::atomic::fetch_or(value, static_cast<elem_t>(t), mo);
}
auto fetch_xor(T t, std::memory_order mo = std::memory_order_seq_cst) -> T {
CPP20(static_assert(
requires { t ^ t; }, "T must support operator^(x, y)"));
return ::atomic::fetch_xor(value, static_cast<elem_t>(t), mo);
}

auto operator&=(T t) -> T { return fetch_and(t) & t; }
auto operator|=(T t) -> T { return fetch_or(t) | t; }
auto operator^=(T t) -> T { return fetch_xor(t) ^ t; }
};
} // namespace v1
} // namespace stdx
6 changes: 6 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ add_tests(
FILES
algorithm
always_false
atomic
atomic_override
atomic_bitset
atomic_bitset_override
bind
Expand Down Expand Up @@ -65,6 +67,10 @@ target_compile_definitions(
atomic_bitset_override_test
PRIVATE -DATOMIC_CFG="${CMAKE_CURRENT_LIST_DIR}/detail/atomic_cfg.hpp")

target_compile_definitions(
atomic_override_test
PRIVATE -DATOMIC_CFG="${CMAKE_CURRENT_LIST_DIR}/detail/atomic_cfg.hpp")

if(${CMAKE_CXX_STANDARD} GREATER_EQUAL 20)
add_tests(FILES ct_format ct_string indexed_tuple tuple tuple_algorithms)
endif()
Expand Down
161 changes: 161 additions & 0 deletions test/atomic.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
#include <stdx/atomic.hpp>

#include <catch2/catch_template_test_macros.hpp>
#include <catch2/catch_test_macros.hpp>

#include <cstdint>
#include <type_traits>

TEMPLATE_TEST_CASE("atomic size and alignment is the same as the data",
"[atomic]", bool, char, signed char, unsigned char,
short int, unsigned short int, int, unsigned int, long int,
unsigned long int) {
static_assert(sizeof(stdx::atomic<TestType>) == sizeof(TestType));
static_assert(alignof(stdx::atomic<TestType>) == alignof(TestType));
}

TEMPLATE_TEST_CASE("atomic is default constructible when data is", "[atomic]",
bool, char, signed char, unsigned char, short int,
unsigned short int, int, unsigned int, long int,
unsigned long int) {
static_assert(std::is_default_constructible_v<stdx::atomic<TestType>>);
}

namespace {
struct non_dc {
non_dc(int) {}
};
} // namespace

#if __cplusplus >= 202002L
TEST_CASE("atomic is not default constructible when data is not", "[atomic]") {
static_assert(not std::is_default_constructible_v<stdx::atomic<non_dc>>);
}
#endif

TEST_CASE("atomic is not copyable or movable", "[atomic]") {
static_assert(not std::is_copy_constructible_v<stdx::atomic<int>>);
static_assert(not std::is_move_constructible_v<stdx::atomic<int>>);
static_assert(not std::is_copy_assignable_v<stdx::atomic<int>>);
static_assert(not std::is_move_assignable_v<stdx::atomic<int>>);
}

TEMPLATE_TEST_CASE("atomic supports value initialization", "[atomic]", bool,
char, signed char, unsigned char, short int,
unsigned short int, int, unsigned int, long int,
unsigned long int) {
static_assert(std::is_constructible_v<stdx::atomic<TestType>, TestType>);
[[maybe_unused]] auto x = stdx::atomic<TestType>{TestType{}};
}

TEST_CASE("load", "[atomic]") {
stdx::atomic<std::uint32_t> val{17};
CHECK(val.load() == 17);
}

TEST_CASE("store", "[atomic]") {
stdx::atomic<std::uint32_t> val{17};
val.store(1337);
CHECK(val.load() == 1337);
}

TEST_CASE("implicit conversion to T", "[atomic]") {
stdx::atomic<std::uint32_t> val{17};
CHECK(val == 17);
}

TEST_CASE("assignment from T", "[atomic]") {
stdx::atomic<std::uint32_t> val{17};
val = 1337;
CHECK(val == 1337);
}

TEST_CASE("exchange", "[atomic]") {
stdx::atomic<std::uint32_t> val{17};
CHECK(val.exchange(1337) == 17);
CHECK(val.load() == 1337);
}

TEST_CASE("fetch_add", "[atomic]") {
stdx::atomic<std::uint32_t> val{17};
CHECK(val.fetch_add(42) == 17);
CHECK(val.load() == 59);
}

TEST_CASE("fetch_sub", "[atomic]") {
stdx::atomic<std::uint32_t> val{59};
CHECK(val.fetch_sub(42) == 59);
CHECK(val.load() == 17);
}

TEST_CASE("operator +=", "[atomic]") {
stdx::atomic<std::uint32_t> val{17};
CHECK((val += 42) == 59);
CHECK(val.load() == 59);
}

TEST_CASE("operator -=", "[atomic]") {
stdx::atomic<std::uint32_t> val{59};
CHECK((val -= 42) == 17);
CHECK(val.load() == 17);
}

TEST_CASE("pre-increment", "[atomic]") {
stdx::atomic<std::uint32_t> val{17};
CHECK(++val == 18);
CHECK(val.load() == 18);
}

TEST_CASE("post-increment", "[atomic]") {
stdx::atomic<std::uint32_t> val{17};
CHECK(val++ == 17);
CHECK(val.load() == 18);
}

TEST_CASE("pre-decrement", "[atomic]") {
stdx::atomic<std::uint32_t> val{17};
CHECK(--val == 16);
CHECK(val.load() == 16);
}

TEST_CASE("post-decrement", "[atomic]") {
stdx::atomic<std::uint32_t> val{17};
CHECK(val-- == 17);
CHECK(val.load() == 16);
}

TEST_CASE("fetch_and", "[atomic]") {
stdx::atomic<std::uint32_t> val{0b101};
CHECK(val.fetch_and(0b100) == 0b101);
CHECK(val.load() == 0b100);
}

TEST_CASE("fetch_or", "[atomic]") {
stdx::atomic<std::uint32_t> val{0b1};
CHECK(val.fetch_or(0b100) == 0b1);
CHECK(val.load() == 0b101);
}

TEST_CASE("fetch_xor", "[atomic]") {
stdx::atomic<std::uint32_t> val{0b101};
CHECK(val.fetch_xor(0b1) == 0b101);
CHECK(val.load() == 0b100);
}

TEST_CASE("operator &=", "[atomic]") {
stdx::atomic<std::uint32_t> val{0b101};
CHECK((val &= 0b100) == 0b100);
CHECK(val.load() == 0b100);
}

TEST_CASE("operator |=", "[atomic]") {
stdx::atomic<std::uint32_t> val{0b1};
CHECK((val |= 0b100) == 0b101);
CHECK(val.load() == 0b101);
}

TEST_CASE("operator ^=", "[atomic]") {
stdx::atomic<std::uint32_t> val{0b101};
CHECK((val ^= 0b1) == 0b100);
CHECK(val.load() == 0b100);
}
2 changes: 1 addition & 1 deletion test/atomic_bitset.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ TEST_CASE("to_natural returns smallest_uint", "[atomic_bitset]") {
auto bs = stdx::atomic_bitset<4>{stdx::all_bits};
auto value = bs.to_natural();
CHECK(value == 0b1111);
static_assert(std::same_as<decltype(value), std::uint8_t>);
static_assert(std::is_same_v<decltype(value), std::uint8_t>);
}

TEMPLATE_TEST_CASE("construct with a string_view", "[atomic_bitset]",
Expand Down
Loading