diff --git a/CMakeLists.txt b/CMakeLists.txt index cadc45ed..f16b8717 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,6 +59,7 @@ endif() # ===== set(SPARROW_HEADERS + ${SPARROW_INCLUDE_DIR}/sparrow/allocator.hpp ${SPARROW_INCLUDE_DIR}/sparrow/array_data.hpp ${SPARROW_INCLUDE_DIR}/sparrow/buffer.hpp ${SPARROW_INCLUDE_DIR}/sparrow/data_type.hpp diff --git a/include/sparrow/allocator.hpp b/include/sparrow/allocator.hpp new file mode 100644 index 00000000..ae514081 --- /dev/null +++ b/include/sparrow/allocator.hpp @@ -0,0 +1,251 @@ +// Copyright 2024 Man Group Operations Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace sparrow +{ + /* + * allocator concept, based on the requirements specified by the standard: + * https://en.cppreference.com/w/cpp/named_req/Allocator. + */ + template + concept allocator = std::copy_constructible> + and std::move_constructible> + and std::equality_comparable> + and requires(std::remove_cvref_t& alloc, + typename std::remove_cvref_t::value_type* p, + std::size_t n) + { + typename std::remove_cvref_t::value_type; + { alloc.allocate(n) } -> std::same_as::value_type*>; + { alloc.deallocate(p, n) } -> std::same_as; + }; + + /* + * When the allocator A with value_type T satisfies this concept, any_allocator + * can store it as a value in a small buffer instead of having to type-erased it + * (Small Buffer Optimization). + */ + template + concept can_any_allocator_sbo = allocator && + (std::same_as> || std::same_as>); + + /* + * Type erasure class for allocators. This allows to use any kind of allocator + * (standard, polymorphic) without having to expose it as a template parameter. + * + * @tparam T value_type of the allocator + */ + template + class any_allocator + { + public: + + using value_type = T; + + any_allocator(); + any_allocator(const any_allocator& rhs); + any_allocator(any_allocator&&) = default; + + any_allocator& operator=(const any_allocator& rhs) = delete; + any_allocator& operator=(any_allocator&& rhs) = delete; + + template + any_allocator(A&& alloc) + requires (not std::same_as, any_allocator> + and sparrow::allocator) + : m_storage(make_storage(std::forward(alloc))) + { + } + + [[nodiscard]] T* allocate(std::size_t n); + void deallocate(T* p, std::size_t n); + + any_allocator select_on_container_copy_construction() const; + + bool equal(const any_allocator& rhs) const; + + private: + + struct interface + { + [[nodiscard]] virtual T* allocate(std::size_t) = 0; + virtual void deallocate(T*, std::size_t) = 0; + virtual std::unique_ptr clone() const = 0; + virtual bool equal(const interface&) const = 0; + virtual ~interface() = default; + }; + + template + struct impl : interface + { + A m_alloc; + + explicit impl(A alloc) : m_alloc(std::move(alloc)) {} + + [[nodiscard]] T* allocate(std::size_t n) override + { + return m_alloc.allocate(n); + } + + void deallocate(T* p, std::size_t n) override + { + m_alloc.deallocate(p, n); + } + + std::unique_ptr clone() const override + { + return std::make_unique>(m_alloc); + } + + bool equal(const interface& rhs) const override + { + if (std::type_index(typeid(*this)) == std::type_index(typeid(rhs))) + { + return m_alloc == static_cast*>(std::addressof(rhs))->m_alloc; + } + return false; + } + }; + + using storage_type = std::variant + < + std::allocator, + std::pmr::polymorphic_allocator, + std::unique_ptr + >; + + template + std::unique_ptr make_storage(A&& alloc) const + { + return std::make_unique>>(std::forward(alloc)); + } + + template + requires can_any_allocator_sbo + A&& make_storage(A&& alloc) const + { + return std::forward(alloc); + } + + template struct overloaded : Ts... { using Ts::operator()...; }; + // Although not required in C++20, clang needs it to build the code below + template overloaded(Ts...) -> overloaded; + + storage_type copy_storage(const storage_type& rhs) const + { + return std::visit(overloaded { + [](const auto& arg) -> storage_type { return { std::decay_t(arg) }; }, + [](const std::unique_ptr& arg) -> storage_type { return { arg->clone() }; } + }, rhs); + } + + template + decltype(auto) visit_storage(F&& f) + { + return std::visit([&f](auto&& arg) + { + using A = std::decay_t; + if constexpr (can_any_allocator_sbo) + return f(arg); + else + return f(*arg); + }, m_storage); + } + + storage_type m_storage; + }; + + /******************************** + * any_allocator implementation * + ********************************/ + + template + any_allocator::any_allocator() + : m_storage(make_storage(std::allocator())) + { + } + + template + any_allocator::any_allocator(const any_allocator& rhs) + : m_storage(copy_storage(rhs.m_storage)) + { + } + + template + [[nodiscard]] T* any_allocator::allocate(std::size_t n) + { + return visit_storage([n](auto& allocator) { return allocator.allocate(n); }); + } + + template + void any_allocator::deallocate(T* p, std::size_t n) + { + return visit_storage([n, p](auto& allocator) { return allocator.deallocate(p, n); }); + } + + template + any_allocator any_allocator::select_on_container_copy_construction() const + { + return any_allocator(*this); + } + + template + bool any_allocator::equal(const any_allocator& rhs) const + { + // YOLO!! + return std::visit([&rhs](auto&& arg) + { + using A = std::decay_t; + if constexpr (can_any_allocator_sbo) + { + return std::visit([&arg](auto&& arg2) + { + using A2 = std::decay_t; + if constexpr (can_any_allocator_sbo && std::same_as) + return arg == arg2; + else + return false; + }, rhs.m_storage); + } + else + { + return std::visit([&arg](auto&& arg2) + { + using A2 = std::decay_t; + if constexpr (can_any_allocator_sbo) + return false; + else + return arg->equal(*arg2); + }, rhs.m_storage); + } + + }, m_storage); + } + + template + bool operator==(const any_allocator& lhs, const any_allocator& rhs) + { + return lhs.equal(rhs); + } +} + diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 717980e5..d7182ba9 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -34,6 +34,7 @@ endif() set(SPARROW_TESTS_SOURCES main.cpp + test_allocator.cpp test_array_data.cpp test_buffer.cpp test_dynamic_bitset.cpp diff --git a/test/test_allocator.cpp b/test/test_allocator.cpp new file mode 100644 index 00000000..9d5ac6bc --- /dev/null +++ b/test/test_allocator.cpp @@ -0,0 +1,91 @@ +// Copyright 2024 Man Group Operations Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "doctest/doctest.h" + +#include +#include +#include +#include + +#include "sparrow/allocator.hpp" + +TEST_SUITE("any_allocator") +{ + TEST_CASE_TEMPLATE_DEFINE("value semantic", A, value_semantic_id) + { + SUBCASE("constructor") + { + { + sparrow::any_allocator a; + } + { + A alloc; + sparrow::any_allocator a(alloc); + } + } + + SUBCASE("copy constructor") + { + using value_type = typename A::value_type; + + A alloc; + sparrow::any_allocator a(alloc); + sparrow::any_allocator b(a); + CHECK(a == b); + + auto d = b.select_on_container_copy_construction(); + CHECK(d == b); + } + + SUBCASE("move constructor") + { + using value_type = typename A::value_type; + + A alloc; + sparrow::any_allocator a(alloc); + sparrow::any_allocator aref(a); + sparrow::any_allocator b(std::move(a)); + CHECK_EQ(b, aref); + } + } + + TEST_CASE_TEMPLATE_DEFINE("allocate / deallocate", A, allocate_id) + { + using value_type = typename A::value_type; + + constexpr std::size_t n = 100; + std::vector ref(n); + std::iota(ref.begin(), ref.end(), value_type()); + + A alloc; + sparrow::any_allocator a(alloc); + value_type* buf = a.allocate(n); + std::uninitialized_copy(ref.cbegin(), ref.cend(), buf); + CHECK_EQ(*buf, ref[0]); + CHECK_EQ(*(buf + n - 1), ref.back()); + a.deallocate(buf, n); + } + +#if __APPLE__ + // /usr/lib/libc++.1.dylib is missing the symbol __ZNSt3__13pmr20get_default_resourceEv, leading + // to an exception at runtime. + TEST_CASE_TEMPLATE_INVOKE(value_semantic_id, std::allocator/*, std::pmr::polymorphic_allocator*/); + TEST_CASE_TEMPLATE_INVOKE(allocate_id, std::allocator/*, std::pmr::polymorphic_allocator*/); +#else + TEST_CASE_TEMPLATE_INVOKE(value_semantic_id, std::allocator, std::pmr::polymorphic_allocator); + TEST_CASE_TEMPLATE_INVOKE(allocate_id, std::allocator, std::pmr::polymorphic_allocator); +#endif +} +