Skip to content

Commit

Permalink
Implemented type erasure for allocators (#55)
Browse files Browse the repository at this point in the history
Implemented type erasure for allocators
  • Loading branch information
JohanMabille authored Apr 9, 2024
1 parent c938478 commit 9999445
Show file tree
Hide file tree
Showing 4 changed files with 344 additions and 0 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
251 changes: 251 additions & 0 deletions include/sparrow/allocator.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
// Copyright 2024 Man Group Operations Limited

Check notice on line 1 in include/sparrow/allocator.hpp

View workflow job for this annotation

GitHub Actions / build

Run clang-format on include/sparrow/allocator.hpp

File include/sparrow/allocator.hpp does not conform to Custom style guidelines. (lines 17, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 49, 50, 73, 74, 81, 85, 103, 130, 131, 132, 133, 134, 143, 150, 152, 156, 157, 158, 159, 165, 166, 167, 168, 169, 170, 171, 172, 197, 203, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 242, 250)
//
// 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 <cstdint>
#include <memory>
#include <memory_resource>
#include <typeindex>
#include <type_traits>
#include <variant>

namespace sparrow
{
/*
* allocator concept, based on the requirements specified by the standard:
* https://en.cppreference.com/w/cpp/named_req/Allocator.
*/
template <class T>
concept allocator = std::copy_constructible<std::remove_cvref_t<T>>
and std::move_constructible<std::remove_cvref_t<T>>
and std::equality_comparable<std::remove_cvref_t<T>>
and requires(std::remove_cvref_t<T>& alloc,
typename std::remove_cvref_t<T>::value_type* p,
std::size_t n)
{
typename std::remove_cvref_t<T>::value_type;
{ alloc.allocate(n) } -> std::same_as<typename std::remove_cvref_t<T>::value_type*>;
{ alloc.deallocate(p, n) } -> std::same_as<void>;
};

/*
* 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 <class A, class T>
concept can_any_allocator_sbo = allocator<A> &&
(std::same_as<A, std::allocator<T>> || std::same_as<A, std::pmr::polymorphic_allocator<T>>);

/*
* 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 T>
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 <class A>
any_allocator(A&& alloc)
requires (not std::same_as<std::remove_cvref_t<A>, any_allocator>
and sparrow::allocator<A>)
: m_storage(make_storage(std::forward<A>(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<interface> clone() const = 0;
virtual bool equal(const interface&) const = 0;
virtual ~interface() = default;
};

template <allocator A>
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<interface> clone() const override
{
return std::make_unique<impl<A>>(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<const impl<A>*>(std::addressof(rhs))->m_alloc;
}
return false;
}
};

using storage_type = std::variant
<
std::allocator<T>,
std::pmr::polymorphic_allocator<T>,
std::unique_ptr<interface>
>;

template <class A>
std::unique_ptr<interface> make_storage(A&& alloc) const
{
return std::make_unique<impl<std::decay_t<A>>>(std::forward<A>(alloc));
}

template <class A>
requires can_any_allocator_sbo<A, T>
A&& make_storage(A&& alloc) const
{
return std::forward<A>(alloc);
}

template <class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
// Although not required in C++20, clang needs it to build the code below
template <class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

storage_type copy_storage(const storage_type& rhs) const
{
return std::visit(overloaded {
[](const auto& arg) -> storage_type { return { std::decay_t<decltype(arg)>(arg) }; },
[](const std::unique_ptr<interface>& arg) -> storage_type { return { arg->clone() }; }
}, rhs);
}

template <class F>
decltype(auto) visit_storage(F&& f)
{
return std::visit([&f](auto&& arg)
{
using A = std::decay_t<decltype(arg)>;
if constexpr (can_any_allocator_sbo<A, T>)
return f(arg);
else
return f(*arg);
}, m_storage);
}

storage_type m_storage;
};

/********************************
* any_allocator implementation *
********************************/

template <class T>
any_allocator<T>::any_allocator()
: m_storage(make_storage(std::allocator<T>()))
{
}

template <class T>
any_allocator<T>::any_allocator(const any_allocator& rhs)
: m_storage(copy_storage(rhs.m_storage))
{
}

template <class T>
[[nodiscard]] T* any_allocator<T>::allocate(std::size_t n)
{
return visit_storage([n](auto& allocator) { return allocator.allocate(n); });
}

template <class T>
void any_allocator<T>::deallocate(T* p, std::size_t n)
{
return visit_storage([n, p](auto& allocator) { return allocator.deallocate(p, n); });
}

template <class T>
any_allocator<T> any_allocator<T>::select_on_container_copy_construction() const
{
return any_allocator(*this);
}

template <class T>
bool any_allocator<T>::equal(const any_allocator& rhs) const
{
// YOLO!!
return std::visit([&rhs](auto&& arg)
{
using A = std::decay_t<decltype(arg)>;
if constexpr (can_any_allocator_sbo<A, T>)
{
return std::visit([&arg](auto&& arg2)
{
using A2 = std::decay_t<decltype(arg2)>;
if constexpr (can_any_allocator_sbo<A2, T> && std::same_as<A, A2>)
return arg == arg2;
else
return false;
}, rhs.m_storage);
}
else
{
return std::visit([&arg](auto&& arg2)
{
using A2 = std::decay_t<decltype(arg2)>;
if constexpr (can_any_allocator_sbo<A2, T>)
return false;
else
return arg->equal(*arg2);
}, rhs.m_storage);
}

}, m_storage);
}

template <class T>
bool operator==(const any_allocator<T>& lhs, const any_allocator<T>& rhs)
{
return lhs.equal(rhs);
}
}

1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
91 changes: 91 additions & 0 deletions test/test_allocator.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2024 Man Group Operations Limited

Check notice on line 1 in test/test_allocator.cpp

View workflow job for this annotation

GitHub Actions / build

Run clang-format on test/test_allocator.cpp

File test/test_allocator.cpp does not conform to Custom style guidelines. (lines 15, 84, 85, 90)
//
// 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 <memory>
#include <memory_resource>
#include <numeric>
#include <vector>

#include "sparrow/allocator.hpp"

TEST_SUITE("any_allocator")
{
TEST_CASE_TEMPLATE_DEFINE("value semantic", A, value_semantic_id)
{
SUBCASE("constructor")
{
{
sparrow::any_allocator<int> a;
}
{
A alloc;
sparrow::any_allocator<typename A::value_type> a(alloc);
}
}

SUBCASE("copy constructor")
{
using value_type = typename A::value_type;

A alloc;
sparrow::any_allocator<value_type> a(alloc);
sparrow::any_allocator<value_type> 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<value_type> a(alloc);
sparrow::any_allocator<value_type> aref(a);
sparrow::any_allocator<value_type> 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<value_type> ref(n);
std::iota(ref.begin(), ref.end(), value_type());

A alloc;
sparrow::any_allocator<value_type> 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<int>/*, std::pmr::polymorphic_allocator<int>*/);
TEST_CASE_TEMPLATE_INVOKE(allocate_id, std::allocator<int>/*, std::pmr::polymorphic_allocator<int>*/);
#else
TEST_CASE_TEMPLATE_INVOKE(value_semantic_id, std::allocator<int>, std::pmr::polymorphic_allocator<int>);
TEST_CASE_TEMPLATE_INVOKE(allocate_id, std::allocator<int>, std::pmr::polymorphic_allocator<int>);
#endif
}

0 comments on commit 9999445

Please sign in to comment.