diff --git a/.github/workflows/code_testing.yaml b/.github/workflows/code_testing.yaml index d7a1256..8b1b7f1 100644 --- a/.github/workflows/code_testing.yaml +++ b/.github/workflows/code_testing.yaml @@ -64,7 +64,7 @@ jobs: uses: dice-group/cpp-conan-release-reusable-workflow/.github/actions/add_conan_provider@main - name: Configure CMake - run: cmake -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined" -DCMAKE_BUILD_TYPE=Debug -DWITH_SVECTOR=ON -DWITH_BOOST=ON -DBUILD_TESTING=On -DBUILD_EXAMPLES=On -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=conan_provider.cmake -G Ninja -B build . + run: cmake -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined" -DCMAKE_BUILD_TYPE=Debug -DWITH_SVECTOR=ON -DWITH_BOOST=ON -DBUILD_TESTING=On -DBUILD_EXAMPLES=On -DCMAKE_COMPILE_WARNING_AS_ERROR=ON -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=conan_provider.cmake -G Ninja -B build . env: CC: ${{ steps.install_cc.outputs.cc }} CXX: ${{ steps.install_cc.outputs.cxx }} diff --git a/README.md b/README.md index 22c5724..81e8c39 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ It contains: - `integral_template_variant`: A wrapper type for `std::variant` guarantees to only contain variants of the form `T` where $\texttt{ix}\in [\texttt{first},\texttt{last}]$ (inclusive). - `for_{types,values,range}`: Compile time for loops for types, values or ranges - `polymorphic_allocator`: Like `std::pmr::polymorphic_allocator` but with static dispatch +- `limit_allocator`: Allocator wrapper that limits the amount of memory that is allowed to be allocated - `DICE_DEFER`/`DICE_DEFER_TO_SUCCES`/`DICE_DEFER_TO_FAIL`: On-the-fly RAII for types that do not support it natively (similar to go's `defer` keyword) - `overloaded`: Composition for `std::variant` visitor lambdas - `flex_array`: A combination of `std::array`, `std::span` and a `vector` with small buffer optimization @@ -64,6 +65,10 @@ The problem with `mmap` allocations is that they will be placed at an arbitrary therefore absolute pointers will cause segfaults if the segment is reloaded. Which means: vtables will not work (because they use absolute pointers) and therefore you cannot use `std::pmr::polymorphic_allocator`. +### `limit_allocator` +Allocator wrapper that limits the amount of memory that can be allocated through the inner allocator. +If the limit is exceeded it will throw `std::bad_alloc`. + ### `DICE_DEFER`/`DICE_DEFER_TO_SUCCES`/`DICE_DEFER_TO_FAIL` A mechanism similar to go's `defer` keyword, which can be used to defer some action to scope exit. The primary use-case for this is on-the-fly RAII-like resource management for types that do not support RAII (for example C types). diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 6a98147..4cdf302 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -80,3 +80,10 @@ target_link_libraries(example_variant2 dice-template-library::dice-template-library ) +add_executable(example_limit_allocator + example_limit_allocator.cpp) +target_link_libraries(example_limit_allocator + PRIVATE + dice-template-library::dice-template-library +) + diff --git a/examples/example_limit_allocator.cpp b/examples/example_limit_allocator.cpp new file mode 100644 index 0000000..1a22112 --- /dev/null +++ b/examples/example_limit_allocator.cpp @@ -0,0 +1,26 @@ +#include + +#include +#include + + +int main() { + std::vector> vec{dice::template_library::limit_allocator{3 * sizeof(int)}}; + vec.push_back(1); + vec.push_back(2); + + try { + vec.push_back(3); + assert(false); + } catch (...) { + } + + vec.pop_back(); + vec.push_back(4); + + try { + vec.push_back(5); + assert(false); + } catch (...) { + } +} diff --git a/include/dice/template-library/limit_allocator.hpp b/include/dice/template-library/limit_allocator.hpp new file mode 100644 index 0000000..9c5aaf7 --- /dev/null +++ b/include/dice/template-library/limit_allocator.hpp @@ -0,0 +1,176 @@ +#ifndef DICE_TEMPLATELIBRARY_LIMITALLOCATOR_HPP +#define DICE_TEMPLATELIBRARY_LIMITALLOCATOR_HPP + +#include +#include +#include +#include +#include +#include + +namespace dice::template_library { + + /** + * The synchronization policy of a limit_allocator + */ + enum struct limit_allocator_syncness : bool { + sync, ///< thread-safe (synchronized) + unsync, ///< not thread-safe (unsynchronized) + }; + + namespace detail_limit_allocator { + template + struct limit_allocator_control_block; + + template<> + struct limit_allocator_control_block { + std::atomic bytes_left = 0; + + void allocate(size_t n_bytes) { + auto old = bytes_left.load(std::memory_order_relaxed); + + do { + if (old < n_bytes) [[unlikely]] { + throw std::bad_alloc{}; + } + } while (!bytes_left.compare_exchange_weak(old, old - n_bytes, std::memory_order_relaxed, std::memory_order_relaxed)); + } + + void deallocate(size_t n_bytes) noexcept { + bytes_left.fetch_add(n_bytes, std::memory_order_relaxed); + } + }; + + template<> + struct limit_allocator_control_block { + size_t bytes_left = 0; + + void allocate(size_t n_bytes) { + if (bytes_left < n_bytes) [[unlikely]] { + throw std::bad_alloc{}; + } + bytes_left -= n_bytes; + } + + void deallocate(size_t n_bytes) noexcept { + bytes_left += n_bytes; + } + }; + }// namespace detail_limit_allocator + + /** + * Allocator wrapper that limits the amount of memory its underlying allocator + * is allowed to allocate. + * + * @tparam T value type of the allocator (the thing that it allocates) + * @tparam Allocator the underlying allocator + * @tparam syncness determines the synchronization of the limit + */ + template typename Allocator = std::allocator, limit_allocator_syncness syncness = limit_allocator_syncness::sync> + struct limit_allocator { + using control_block_type = detail_limit_allocator::limit_allocator_control_block; + using value_type = T; + using upstream_allocator_type = Allocator; + using pointer = typename std::allocator_traits::pointer; + using const_pointer = typename std::allocator_traits::const_pointer; + using void_pointer = typename std::allocator_traits::void_pointer; + using const_void_pointer = typename std::allocator_traits::const_void_pointer; + using size_type = typename std::allocator_traits::size_type; + using difference_type = typename std::allocator_traits::difference_type; + + using propagate_on_container_copy_assignment = typename std::allocator_traits::propagate_on_container_copy_assignment; + using propagate_on_container_move_assignment = typename std::allocator_traits::propagate_on_container_move_assignment; + using propagate_on_container_swap = typename std::allocator_traits::propagate_on_container_swap; + using is_always_equal = std::false_type; + + template + struct rebind { + using other = limit_allocator; + }; + + private: + template typename, limit_allocator_syncness> + friend struct limit_allocator; + + std::shared_ptr control_block_; + [[no_unique_address]] upstream_allocator_type inner_; + + constexpr limit_allocator(std::shared_ptr const &control_block, upstream_allocator_type const &alloc) + requires(std::is_default_constructible_v) + : control_block_{control_block}, + inner_{alloc} { + } + + public: + explicit constexpr limit_allocator(size_t bytes_limit) + requires(std::is_default_constructible_v) + : control_block_{std::make_shared(bytes_limit)}, + inner_{} { + } + + constexpr limit_allocator(limit_allocator const &other) noexcept(std::is_nothrow_move_constructible_v) = default; + constexpr limit_allocator(limit_allocator &&other) noexcept(std::is_nothrow_copy_constructible_v) = default; + constexpr limit_allocator &operator=(limit_allocator const &other) noexcept(std::is_nothrow_copy_assignable_v) = default; + constexpr limit_allocator &operator=(limit_allocator &&other) noexcept(std::is_nothrow_move_assignable_v) = default; + constexpr ~limit_allocator() = default; + + template + constexpr limit_allocator(limit_allocator const &other) noexcept(std::is_nothrow_constructible_v::upstream_allocator_type const &>) + : control_block_{other.control_block_}, + inner_{other.inner_} { + } + + constexpr limit_allocator(size_t bytes_limit, upstream_allocator_type const &upstream) + : control_block_{std::make_shared(bytes_limit)}, + inner_{upstream} { + } + + constexpr limit_allocator(size_t bytes_limit, upstream_allocator_type &&upstream) + : control_block_{std::make_shared(bytes_limit)}, + inner_{std::move(upstream)} { + } + + template + explicit constexpr limit_allocator(size_t bytes_limit, std::in_place_t, Args &&...args) + : control_block_{std::make_shared(bytes_limit)}, + inner_{std::forward(args)...} { + } + + constexpr pointer allocate(size_t n) { + control_block_->allocate(n * sizeof(T)); + + try { + return std::allocator_traits::allocate(inner_, n); + } catch (...) { + control_block_->deallocate(n * sizeof(T)); + throw; + } + } + + constexpr void deallocate(pointer ptr, size_t n) { + std::allocator_traits::deallocate(inner_, ptr, n); + control_block_->deallocate(n * sizeof(T)); + } + + constexpr limit_allocator select_on_container_copy_construction() const { + return limit_allocator{control_block_, std::allocator_traits::select_on_container_copy_construction(inner_)}; + } + + [[nodiscard]] upstream_allocator_type const &upstream_allocator() const noexcept { + return inner_; + } + + friend constexpr void swap(limit_allocator &a, limit_allocator &b) noexcept(std::is_nothrow_swappable_v) + requires(std::is_swappable_v) + { + using std::swap; + swap(a.control_block_, b.control_block_); + swap(a.inner_, b.inner_); + } + + bool operator==(limit_allocator const &other) const noexcept = default; + bool operator!=(limit_allocator const &other) const noexcept = default; + }; +}// namespace dice::template_library + +#endif// DICE_TEMPLATELIBRARY_LIMITALLOCATOR_HPP \ No newline at end of file diff --git a/include/dice/template-library/polymorphic_allocator.hpp b/include/dice/template-library/polymorphic_allocator.hpp index 9cc3e0c..3d5afae 100644 --- a/include/dice/template-library/polymorphic_allocator.hpp +++ b/include/dice/template-library/polymorphic_allocator.hpp @@ -337,6 +337,11 @@ namespace dice::template_library { using propagate_on_container_swap = typename std::allocator_traits::propagate_on_container_swap; using is_always_equal = typename std::allocator_traits::is_always_equal; + template + struct rebind { + using other = offset_ptr_stl_allocator; + }; + private: template typename> friend struct offset_ptr_stl_allocator; diff --git a/include/dice/template-library/variant2.hpp b/include/dice/template-library/variant2.hpp index 50f18e3..a208936 100644 --- a/include/dice/template-library/variant2.hpp +++ b/include/dice/template-library/variant2.hpp @@ -10,6 +10,19 @@ #include #include +#define DICE_TEMPLATELIBRARY_DETAIL_VARIANT2_TRY(noexcept_spec, action_block) \ + if constexpr (noexcept_spec) { \ + action_block \ + } else { \ + try { \ + action_block \ + } catch (...) { \ + discriminant_ = discriminant_type::ValuelessByException; \ + throw; \ + } \ + } + + namespace dice::template_library { template struct variant2; @@ -314,13 +327,10 @@ namespace dice::template_library { break; } case discriminant_type::Second: { - try { - a_.~T(); + a_.~T(); + DICE_TEMPLATELIBRARY_DETAIL_VARIANT2_TRY(std::is_nothrow_copy_constructible_v, { new (&b_) U{other.b_}; - } catch (...) { - discriminant_ = discriminant_type::ValuelessByException; - throw; - } + }); break; } case discriminant_type::ValuelessByException: { @@ -337,13 +347,10 @@ namespace dice::template_library { case discriminant_type::Second: { switch (other.discriminant_) { case discriminant_type::First: { - try { - b_.~U(); + b_.~U(); + DICE_TEMPLATELIBRARY_DETAIL_VARIANT2_TRY(std::is_nothrow_copy_constructible_v, { new (&a_) T{other.a_}; - } catch (...) { - discriminant_ = discriminant_type::ValuelessByException; - throw; - } + }); break; } case discriminant_type::Second: { @@ -410,12 +417,9 @@ namespace dice::template_library { } case discriminant_type::Second: { a_.~T(); - try { + DICE_TEMPLATELIBRARY_DETAIL_VARIANT2_TRY(std::is_nothrow_move_constructible_v, { new (&b_) U{std::move(other.b_)}; - } catch (...) { - discriminant_ = discriminant_type::ValuelessByException; - throw; - } + }); break; } case discriminant_type::ValuelessByException: { @@ -433,12 +437,9 @@ namespace dice::template_library { switch (other.discriminant_) { case discriminant_type::First: { b_.~U(); - try { + DICE_TEMPLATELIBRARY_DETAIL_VARIANT2_TRY(std::is_nothrow_move_constructible_v, { new (&a_) T{std::move(other.a_)}; - } catch (...) { - discriminant_ = discriminant_type::ValuelessByException; - throw; - } + }); break; } case discriminant_type::Second: { @@ -499,12 +500,9 @@ namespace dice::template_library { } case discriminant_type::Second: { b_.~U(); - try { + DICE_TEMPLATELIBRARY_DETAIL_VARIANT2_TRY(std::is_nothrow_copy_constructible_v, { new (&a_) T{value}; - } catch (...) { - discriminant_ = discriminant_type::ValuelessByException; - throw; - } + }); discriminant_ = discriminant_type::First; break; } @@ -533,12 +531,9 @@ namespace dice::template_library { } case discriminant_type::Second: { b_.~U(); - try { + DICE_TEMPLATELIBRARY_DETAIL_VARIANT2_TRY(std::is_nothrow_move_constructible_v, { new (&a_) T{std::move(value)}; - } catch (...) { - discriminant_ = discriminant_type::ValuelessByException; - throw; - } + }); discriminant_ = discriminant_type::First; break; } @@ -562,12 +557,9 @@ namespace dice::template_library { switch (discriminant_) { case discriminant_type::First: { a_.~T(); - try { + DICE_TEMPLATELIBRARY_DETAIL_VARIANT2_TRY(std::is_nothrow_copy_constructible_v, { new (&b_) U{value}; - } catch (...) { - discriminant_ = discriminant_type::ValuelessByException; - throw; - } + }); discriminant_ = discriminant_type::Second; break; } @@ -594,12 +586,9 @@ namespace dice::template_library { switch (discriminant_) { case discriminant_type::First: { a_.~T(); - try { + DICE_TEMPLATELIBRARY_DETAIL_VARIANT2_TRY(std::is_nothrow_move_constructible_v, { new (&b_) U{std::move(value)}; - } catch (...) { - discriminant_ = discriminant_type::ValuelessByException; - throw; - } + }); discriminant_ = discriminant_type::Second; break; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0799ac2..8f39bea 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -64,5 +64,8 @@ custom_add_test(tests_variant2) add_executable(tests_type_traits tests_type_traits.cpp) custom_add_test(tests_type_traits) +add_executable(tests_limit_allocator tests_limit_allocator.cpp) +custom_add_test(tests_limit_allocator) + add_executable(tests_pool_allocator tests_pool_allocator.cpp) custom_add_test(tests_pool_allocator) diff --git a/tests/tests_limit_allocator.cpp b/tests/tests_limit_allocator.cpp new file mode 100644 index 0000000..7ab342c --- /dev/null +++ b/tests/tests_limit_allocator.cpp @@ -0,0 +1,39 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include + +#include + +TEST_SUITE("limit_allocator sanity check") { + using namespace dice::template_library; + + template + void run_tests() { + limit_allocator alloc{12 * sizeof(int)}; + + auto a = alloc.allocate(5); + auto b = alloc.allocate(5); + auto c = alloc.allocate(1); + alloc.deallocate(c, 1); + alloc.deallocate(b, 5); + + CHECK_THROWS(alloc.allocate(8)); + + auto d = alloc.allocate(7); + + alloc.deallocate(a, 5); + alloc.deallocate(d, 7); + + auto e = alloc.allocate(12); + alloc.deallocate(e, 12); + + CHECK_THROWS(alloc.allocate(13)); + } + + TEST_CASE("sync") { + run_tests(); + } + + TEST_CASE("unsync") { + run_tests(); + } +}