From d5ec42e15a2e9cdfb9127974f2d12c470cda7e9d Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Wed, 7 Dec 2022 15:26:03 +0100 Subject: [PATCH] feat: Any type with configurable small buffer optimization (#1695) --- Core/include/Acts/Utilities/Any.hpp | 505 ++++++++++++++++++ Tests/UnitTests/Core/Utilities/AnyTests.cpp | 494 +++++++++++++++++ Tests/UnitTests/Core/Utilities/CMakeLists.txt | 4 + 3 files changed, 1003 insertions(+) create mode 100644 Core/include/Acts/Utilities/Any.hpp create mode 100644 Tests/UnitTests/Core/Utilities/AnyTests.cpp diff --git a/Core/include/Acts/Utilities/Any.hpp b/Core/include/Acts/Utilities/Any.hpp new file mode 100644 index 00000000000..f7ae3718295 --- /dev/null +++ b/Core/include/Acts/Utilities/Any.hpp @@ -0,0 +1,505 @@ +// This file is part of the Acts project. +// +// Copyright (C) 2021 CERN for the benefit of the Acts project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#pragma once + +#include +#include +#include +#include +#include +#include + +// #define _ACTS_ANY_ENABLE_VERBOSE +// #define _ACTS_ANY_ENABLE_DEBUG +// #define _ACTS_ANY_ENABLE_TRACK_ALLOCATIONS + +#if defined(_ACTS_ANY_ENABLE_TRACK_ALLOCATIONS) +#include +#include +#include +#include +#include +#endif + +#if defined(_ACTS_ANY_ENABLE_VERBOSE) || defined(_ACTS_ANY_ENABLE_DEBUG) +#include +#include +#endif + +#if defined(_ACTS_ANY_ENABLE_DEBUG) +#define _ACTS_ANY_DEBUG(x) std::cout << x << std::endl; +#else +#define _ACTS_ANY_DEBUG(x) +#endif + +#if defined(_ACTS_ANY_ENABLE_VERBOSE) +#define _ACTS_ANY_VERBOSE(x) std::cout << x << std::endl; +#define _ACTS_ANY_VERBOSE_BUFFER(s, b) \ + do { \ + std::cout << "" << s << ": 0x"; \ + for (char c : b) { \ + std::cout << std::hex << (int)c; \ + } \ + std::cout << std::endl; \ + } while (0) +#else +#define _ACTS_ANY_VERBOSE(x) +#define _ACTS_ANY_VERBOSE_BUFFER(s, b) +#endif + +namespace Acts { + +#if defined(_ACTS_ANY_ENABLE_TRACK_ALLOCATIONS) +static std::mutex _s_any_mutex; +static std::set> _s_any_allocations; + +#define _ACTS_ANY_TRACK_ALLOCATION(T, heap) \ + do { \ + std::lock_guard guard{_s_any_mutex}; \ + _s_any_allocations.emplace(std::type_index(typeid(T)), heap); \ + _ACTS_ANY_DEBUG("Allocate type: " << typeid(T).name() << " at " << heap) \ + } while (0) + +#define _ACTS_ANY_TRACK_DEALLOCATION(T, heap) \ + do { \ + std::lock_guard guard{_s_any_mutex}; \ + auto it = \ + _s_any_allocations.find(std::pair{std::type_index(typeid(T)), heap}); \ + if (it == _s_any_allocations.end()) { \ + throw std::runtime_error{ \ + "Trying to deallocate heap address that we didn't allocate"}; \ + } \ + _s_any_allocations.erase(it); \ + } while (0) + +struct _AnyAllocationReporter { + static void checkAllocations() { + std::lock_guard guard{_s_any_mutex}; + + if (!_s_any_allocations.empty()) { + std::cout << "Not all allocations have been released" << std::endl; + for (const auto& [idx, addr] : _s_any_allocations) { + std::cout << "- " << idx.name() << ": " << addr << std::endl; + } + throw std::runtime_error{"AnyCheckAllocations failed"}; + } + } + + ~_AnyAllocationReporter() noexcept { checkAllocations(); } +}; +static _AnyAllocationReporter s_reporter; +#else +#define _ACTS_ANY_TRACK_ALLOCATION(T, heap) \ + do { \ + } while (0) +#define _ACTS_ANY_TRACK_DEALLOCATION(T, heap) \ + do { \ + } while (0) +#endif + +class AnyBaseAll {}; + +/// Small opaque cache type which uses small buffer optimization +template +class AnyBase : public AnyBaseAll { + static_assert(sizeof(void*) <= SIZE, "Size is too small for a pointer"); + + public: + template + explicit AnyBase(std::in_place_type_t /*unused*/, Args&&... args) { + using U = std::decay_t; + static_assert( + std::is_move_assignable_v && std::is_move_constructible_v, + "Type needs to be move assignable and move constructible"); + static_assert( + std::is_copy_assignable_v && std::is_copy_constructible_v, + "Type needs to be copy assignable and copy constructible"); + + m_handler = makeHandler(); + if constexpr (not heapAllocated()) { + // construct into local buffer + /*U* ptr =*/new (m_data.data()) U(std::forward(args)...); + _ACTS_ANY_VERBOSE( + "Construct local (this=" << this << ") at: " << (void*)m_data.data()); + } else { + // too large, heap allocate + U* heap = new U(std::forward(args)...); + _ACTS_ANY_TRACK_ALLOCATION(T, heap); + setDataPtr(heap); + } + } + +#if defined(_ACTS_ANY_ENABLE_VERBOSE) + AnyBase() { _ACTS_ANY_VERBOSE("Default construct this=" << this); }; +#else + AnyBase() = default; +#endif + + template , AnyBase>>> + explicit AnyBase(T&& value) + : AnyBase{std::in_place_type, std::forward(value)} {} + + template + T& as() { + static_assert(std::is_same_v>, + "Please pass the raw type, no const or ref"); + if (makeHandler() != m_handler) { + throw std::bad_any_cast{}; + } + + _ACTS_ANY_VERBOSE("Get as " + << (m_handler->heapAllocated ? "heap" : "local")); + + return *reinterpret_cast(dataPtr()); + } + + template + const T& as() const { + static_assert(std::is_same_v>, + "Please pass the raw type, no const or ref"); + if (makeHandler() != m_handler) { + throw std::bad_any_cast{}; + } + + _ACTS_ANY_VERBOSE("Get as " << (m_handler->heap ? "heap" : "local")); + + return *reinterpret_cast(dataPtr()); + } + + ~AnyBase() { destroy(); } + + AnyBase(const AnyBase& other) { + if (m_handler == nullptr && other.m_handler == nullptr) { + // both are empty, noop + return; + } + + _ACTS_ANY_VERBOSE( + "Copy construct (this=" << this << ") at: " << (void*)m_data.data()); + + m_handler = other.m_handler; + copyConstruct(other); + } + + AnyBase& operator=(const AnyBase& other) { + _ACTS_ANY_VERBOSE("Copy assign (this=" << this + << ") at: " << (void*)m_data.data()); + + if (m_handler == nullptr && other.m_handler == nullptr) { + // both are empty, noop + return *this; + } + + if (m_handler == nullptr) { // this object is empty + m_handler = other.m_handler; + copyConstruct(other); + } else { + // @TODO: Support assigning between different types + if (m_handler != other.m_handler) { + throw std::bad_any_cast{}; + } + copy(other); + } + return *this; + } + + AnyBase(AnyBase&& other) { + _ACTS_ANY_VERBOSE( + "Move construct (this=" << this << ") at: " << (void*)m_data.data()); + if (m_handler == nullptr && other.m_handler == nullptr) { + // both are empty, noop + return; + } + + m_handler = other.m_handler; + moveConstruct(std::move(other)); + } + + AnyBase& operator=(AnyBase&& other) { + _ACTS_ANY_VERBOSE("Move assign (this=" << this + << ") at: " << (void*)m_data.data()); + if (m_handler == nullptr && other.m_handler == nullptr) { + // both are empty, noop + return *this; + } + + if (m_handler == nullptr) { // this object is empty + m_handler = other.m_handler; + moveConstruct(std::move(other)); + } else { + // @TODO: Support assigning between different types + if (m_handler != other.m_handler) { + throw std::bad_any_cast{}; + } + move(std::move(other)); + } + return *this; + } + + operator bool() const { return m_handler != nullptr; } + + private: + void* dataPtr() { + if (m_handler->heapAllocated) { + return *reinterpret_cast(m_data.data()); + } else { + return reinterpret_cast(m_data.data()); + } + } + + void setDataPtr(void* ptr) { *reinterpret_cast(m_data.data()) = ptr; } + + const void* dataPtr() const { + if (m_handler->heapAllocated) { + return *reinterpret_cast(m_data.data()); + } else { + return reinterpret_cast(m_data.data()); + } + } + + struct Handler { + void (*destroy)(void* ptr) = nullptr; + void (*moveConstruct)(void* from, void* to) = nullptr; + void (*move)(void* from, void* to) = nullptr; + void* (*copyConstruct)(const void* from, void* to) = nullptr; + void (*copy)(const void* from, void* to) = nullptr; + bool heapAllocated{false}; + }; + + template + static const Handler* makeHandler() { + static_assert(!std::is_same_v>, "Cannot wrap any in any"); + static const Handler static_handler = []() { + Handler h; + h.heapAllocated = heapAllocated(); + if constexpr (!std::is_trivially_destructible_v || + heapAllocated()) { + h.destroy = &destroyImpl; + } + if constexpr (!std::is_trivially_move_constructible_v || + heapAllocated()) { + h.moveConstruct = &moveConstructImpl; + } + if constexpr (!std::is_trivially_move_assignable_v || + heapAllocated()) { + h.move = &moveImpl; + } + if constexpr (!std::is_trivially_copy_constructible_v || + heapAllocated()) { + h.copyConstruct = ©ConstructImpl; + } + if constexpr (!std::is_trivially_copy_assignable_v || + heapAllocated()) { + h.copy = ©Impl; + } + + _ACTS_ANY_DEBUG("Type: " << typeid(T).name()); + _ACTS_ANY_DEBUG(" -> destroy: " << h.destroy); + _ACTS_ANY_DEBUG(" -> moveConstruct: " << h.moveConstruct); + _ACTS_ANY_DEBUG(" -> move: " << h.move); + _ACTS_ANY_DEBUG(" -> copyConstruct: " << h.copyConstruct); + _ACTS_ANY_DEBUG(" -> copy: " << h.copy); + _ACTS_ANY_DEBUG( + " -> heapAllocated: " << (h.heapAllocated ? "yes" : "no")); + + return h; + }(); + return &static_handler; + } + + template + static constexpr bool heapAllocated() { + return sizeof(T) > SIZE; + } + + void destroy() { + _ACTS_ANY_VERBOSE("Destructor this=" << this << " handler: " << m_handler); + if (m_handler != nullptr && m_handler->destroy != nullptr) { + m_handler->destroy(dataPtr()); + m_handler = nullptr; + } + } + + void moveConstruct(AnyBase&& fromAny) { + if (m_handler == nullptr) { + return; + } + + void* to = dataPtr(); + void* from = fromAny.dataPtr(); + if (m_handler->heapAllocated) { + // stored on heap: just copy the pointer + setDataPtr(fromAny.dataPtr()); + // do not delete in moved-from any + fromAny.m_handler = nullptr; + return; + } + + if (m_handler->moveConstruct == nullptr) { + // trivially move constructible + m_data = std::move(fromAny.m_data); + } else { + m_handler->moveConstruct(from, to); + } + } + + void move(AnyBase&& fromAny) { + if (m_handler == nullptr) { + return; + } + + void* to = dataPtr(); + void* from = fromAny.dataPtr(); + if (m_handler->heapAllocated) { + // stored on heap: just copy the pointer + // need to delete existing pointer + m_handler->destroy(dataPtr()); + setDataPtr(fromAny.dataPtr()); + // do not delete in moved-from any + fromAny.m_handler = nullptr; + return; + } + + if (m_handler->move == nullptr) { + // trivially movable + m_data = std::move(fromAny.m_data); + } else { + m_handler->move(from, to); + } + } + + void copyConstruct(const AnyBase& fromAny) { + if (m_handler == nullptr) { + return; + } + + void* to = dataPtr(); + const void* from = fromAny.dataPtr(); + + if (m_handler->copyConstruct == nullptr) { + // trivially copy constructible + m_data = fromAny.m_data; + } else { + void* copyAt = m_handler->copyConstruct(from, to); + if (to == nullptr) { + assert(copyAt != nullptr); + // copy allocated, store pointer + setDataPtr(copyAt); + } + } + } + + void copy(const AnyBase& fromAny) { + if (m_handler == nullptr) { + return; + } + + void* to = dataPtr(); + const void* from = fromAny.dataPtr(); + + if (m_handler->copy == nullptr) { + // trivially copyable + m_data = fromAny.m_data; + } else { + m_handler->copy(from, to); + } + } + + template + static void destroyImpl(void* ptr) { + assert(ptr != nullptr && "Address to destroy is nullptr"); + T* obj = static_cast(ptr); + if constexpr (!heapAllocated()) { + // stored in place: just call the destructor + _ACTS_ANY_VERBOSE("Destroy local at: " << ptr); + obj->~T(); + } else { + // stored on heap: delete + _ACTS_ANY_DEBUG("Delete type: " << typeid(T).name() + << " heap at: " << obj); + _ACTS_ANY_TRACK_DEALLOCATION(T, obj); + delete obj; + } + } + + template + static void moveConstructImpl(void* from, void* to) { + _ACTS_ANY_VERBOSE("move const: " << from << " -> " << to); + assert(from != nullptr && "Source is null"); + assert(to != nullptr && "Target is null"); + T* _from = static_cast(from); + /*T* ptr =*/new (to) T(std::move(*_from)); + } + + template + static void moveImpl(void* from, void* to) { + _ACTS_ANY_VERBOSE("move: " << from << " -> " << to); + assert(from != nullptr && "Source is null"); + assert(to != nullptr && "Target is null"); + + T* _from = static_cast(from); + T* _to = static_cast(to); + + (*_to) = std::move(*_from); + } + + template + static void* copyConstructImpl(const void* from, void* to) { + _ACTS_ANY_VERBOSE("copy const: " << from << " -> " << to); + assert(from != nullptr && "Source is null"); + const T* _from = static_cast(from); + if (to == nullptr) { + assert(heapAllocated() && "Received nullptr in local buffer case"); + to = new T(*_from); + _ACTS_ANY_TRACK_ALLOCATION(T, to); + + } else { + assert(!heapAllocated() && "Received non-nullptr in heap case"); + /*T* ptr =*/new (to) T(*_from); + } + return to; + } + + template + static void copyImpl(const void* from, void* to) { + _ACTS_ANY_VERBOSE("copy: " << from << " -> " << to); + assert(from != nullptr && "Source is null"); + assert(to != nullptr && "Target is null"); + + const T* _from = static_cast(from); + T* _to = static_cast(to); + + (*_to) = *_from; + } + + static constexpr size_t kMaxAlignment = std::max(alignof(std::max_align_t), +#if defined(__AVX512F__) + size_t(64) +#elif defined(__AVX__) + size_t(32) +#elif defined(__SSE__) + size_t(16) +#else + size_t(0) // Neutral element + // for maximum +#endif + ); + + alignas(kMaxAlignment) std::array m_data{}; + const Handler* m_handler{nullptr}; +}; + +using Any = AnyBase; + +#undef _ACTS_ANY_VERBOSE +#undef _ACTS_ANY_VERBOSE_BUFFER +#undef _ACTS_ANY_ENABLE_VERBOSE + +} // namespace Acts diff --git a/Tests/UnitTests/Core/Utilities/AnyTests.cpp b/Tests/UnitTests/Core/Utilities/AnyTests.cpp new file mode 100644 index 00000000000..8a99a3cbd98 --- /dev/null +++ b/Tests/UnitTests/Core/Utilities/AnyTests.cpp @@ -0,0 +1,494 @@ +// This file is part of the Acts project. +// +// Copyright (C) 2021 CERN for the benefit of the Acts project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include + +#include "Acts/Utilities/Any.hpp" + +#include + +using namespace Acts; + +#if defined(_ACTS_ANY_ENABLE_TRACK_ALLOCATIONS) +#define CHECK_ANY_ALLOCATIONS() \ + do { \ + _AnyAllocationReporter::checkAllocations(); \ + } while (0) +#else +#define CHECK_ANY_ALLOCATIONS() \ + do { \ + } while (0) +#endif + +BOOST_AUTO_TEST_SUITE(AnyTests) + +BOOST_AUTO_TEST_CASE(AnyConstructPrimitive) { + { + // small type + Any a; + BOOST_CHECK(!a); + + int v = 5; + a = Any{v}; + BOOST_CHECK(!!a); + + BOOST_CHECK_EQUAL(a.as(), v); + BOOST_CHECK_NE(a.as(), v + 1); + + BOOST_CHECK_THROW(a.as(), std::bad_any_cast); + BOOST_CHECK_THROW(a = Any{0.5f}, std::bad_any_cast); + } + CHECK_ANY_ALLOCATIONS(); + + { + // type that is large + Any a; + BOOST_CHECK(!a); + + std::array v{1, 2}; + a = Any{v}; + BOOST_CHECK(!!a); + + BOOST_CHECK_EQUAL_COLLECTIONS(a.as().begin(), + a.as().end(), v.begin(), + v.end()); + BOOST_CHECK_THROW(a.as(), std::bad_any_cast); + BOOST_CHECK_THROW(a = Any{0.5f}, std::bad_any_cast); + } + CHECK_ANY_ALLOCATIONS(); + + { + // type that is large + Any a; + BOOST_CHECK(!a); + + std::array v{1, 2, 3, 4, 5}; + a = Any{v}; + BOOST_CHECK(!!a); + + BOOST_CHECK_EQUAL_COLLECTIONS(a.as().begin(), + a.as().end(), v.begin(), + v.end()); + BOOST_CHECK_THROW(a.as(), std::bad_any_cast); + BOOST_CHECK_THROW(a = Any{0.5f}, std::bad_any_cast); + } + CHECK_ANY_ALLOCATIONS(); +} + +BOOST_AUTO_TEST_CASE(AnyAssignConstructEmpty) { + Any a; + Any b; + a = b; + Any c{a}; + a = std::move(b); + Any d{std::move(a)}; + + BOOST_CHECK(!a); + BOOST_CHECK(!b); + BOOST_CHECK(!c); + BOOST_CHECK(!d); + + CHECK_ANY_ALLOCATIONS(); +} + +BOOST_AUTO_TEST_CASE(AnyConstructCustom) { + struct A { + int value; + A() { value = 76; } + }; + + Any a; + BOOST_CHECK(!a); + a = Any{A{}}; + + BOOST_CHECK(!!a); + + BOOST_CHECK_EQUAL(a.as().value, 76); + + CHECK_ANY_ALLOCATIONS(); +} + +BOOST_AUTO_TEST_CASE(AnyConstructCustomInPlace) { + struct A { + int value; + A(int v) { value = v; } + }; + + Any a{std::in_place_type, 42}; + BOOST_CHECK(!!a); + BOOST_CHECK_EQUAL(a.as().value, 42); + + CHECK_ANY_ALLOCATIONS(); +} + +BOOST_AUTO_TEST_CASE(AnyMove) { + { + // small type + Any a; + BOOST_CHECK(!a); + + int v = 5; + a = Any{v}; + BOOST_CHECK(!!a); + + Any b = std::move(a); + BOOST_CHECK(!!b); + BOOST_CHECK_EQUAL(b.as(), 5); + + Any c; + c = std::move(b); + BOOST_CHECK(!!c); + BOOST_CHECK_EQUAL(c.as(), 5); + } + + CHECK_ANY_ALLOCATIONS(); +} + +BOOST_AUTO_TEST_CASE(AnyCopy) { + { + // small type + Any a; + BOOST_CHECK(!a); + + int v = 5; + a = Any{v}; + BOOST_CHECK(!!a); + + Any b = a; + BOOST_CHECK(!!b); + BOOST_CHECK_EQUAL(b.as(), 5); + + Any c; + c = a; + BOOST_CHECK(!!c); + BOOST_CHECK_EQUAL(c.as(), 5); + } + CHECK_ANY_ALLOCATIONS(); +} + +struct D { + bool* destroyed; + D(bool* d) : destroyed{d} {} + ~D() { *destroyed = true; } +}; + +struct D2 { + bool* destroyed{nullptr}; + std::array blob{}; + + D2(bool* d) : destroyed{d} {} + + ~D2() { *destroyed = true; } +}; + +BOOST_AUTO_TEST_CASE(AnyDestroy) { + { // small type + bool destroyed = false; + D d{&destroyed}; + BOOST_CHECK(!destroyed); + { + Any a{std::move(d)}; + BOOST_CHECK(!destroyed); + } + BOOST_CHECK(destroyed); + } + CHECK_ANY_ALLOCATIONS(); + + { // large type + bool destroyed = false; + D2 d{&destroyed}; + BOOST_CHECK(!destroyed); + { + Any a{std::move(d)}; + BOOST_CHECK(!destroyed); + } + BOOST_CHECK(destroyed); + } + CHECK_ANY_ALLOCATIONS(); +} + +BOOST_AUTO_TEST_CASE(AnyDestroyCopy) { + { // small type + bool destroyed = false; + + { + Any b; + { + Any a{std::in_place_type, &destroyed}; + BOOST_CHECK(!destroyed); + b = a; + BOOST_CHECK(!destroyed); + } + BOOST_CHECK(destroyed); // a destroyed, should be true + destroyed = false; + BOOST_CHECK(!destroyed); + } + BOOST_CHECK(destroyed); // b destroyed, should be true again + } + CHECK_ANY_ALLOCATIONS(); + + { // large type + bool destroyed = false; + + { + Any b; + { + Any a{std::in_place_type, &destroyed}; + BOOST_CHECK(!destroyed); + b = a; + BOOST_CHECK(!destroyed); + } + BOOST_CHECK(destroyed); // a destroyed, should be true + destroyed = false; + BOOST_CHECK(!destroyed); + } + BOOST_CHECK(destroyed); // b destroyed, should be true again + } + CHECK_ANY_ALLOCATIONS(); +} + +BOOST_AUTO_TEST_CASE(AnyDestroyInPlace) { + { // small type + bool destroyed = false; + BOOST_CHECK(!destroyed); + { + Any a{std::in_place_type, &destroyed}; + BOOST_CHECK(!destroyed); + } + BOOST_CHECK(destroyed); + } + CHECK_ANY_ALLOCATIONS(); + + { // large type + bool destroyed = false; + BOOST_CHECK(!destroyed); + { + Any a{std::in_place_type, &destroyed}; + BOOST_CHECK(!destroyed); + } + BOOST_CHECK(destroyed); + } + CHECK_ANY_ALLOCATIONS(); +} + +struct D3 { + size_t* destroyed{nullptr}; + std::array blob{}; + + D3(size_t* d) : destroyed{d} {} + + ~D3() { (*destroyed)++; } +}; + +BOOST_AUTO_TEST_CASE(LeakCheck) { + size_t destroyed = 0; + for (size_t i = 0; i < 10000; i++) { + { + BOOST_CHECK_EQUAL(destroyed, i); + Any a; + BOOST_CHECK_EQUAL(destroyed, i); + a = Any{std::in_place_type, &destroyed}; + BOOST_CHECK_EQUAL(destroyed, i); + } + BOOST_CHECK_EQUAL(destroyed, i + 1); + } + CHECK_ANY_ALLOCATIONS(); +} + +struct LifecycleCounters { + size_t nDestroy = 0; + size_t nCopyConstruct = 0; + size_t nCopy = 0; + size_t nMoveConstruct = 0; + size_t nMove = 0; +}; + +template +struct Lifecycle; + +template <> +struct Lifecycle<0> { + LifecycleCounters* counters; + + Lifecycle(LifecycleCounters* _counters) : counters{_counters} {} + + Lifecycle(Lifecycle&& o) { + counters = o.counters; + counters->nMoveConstruct++; + } + + Lifecycle& operator=(Lifecycle&& o) { + counters = o.counters; + counters->nMove++; + return *this; + } + + Lifecycle(const Lifecycle& o) { + counters = o.counters; + counters->nCopyConstruct++; + } + + Lifecycle& operator=(const Lifecycle& o) { + counters = o.counters; + counters->nCopy++; + return *this; + } + + ~Lifecycle() { counters->nDestroy++; } +}; + +template +struct Lifecycle : public Lifecycle<0> { + std::array m_padding{}; + + Lifecycle(LifecycleCounters* _counters) : Lifecycle<0>(_counters) {} +}; + +template +struct LifecycleHandle { + LifecycleCounters counters; + Lifecycle inner; + + LifecycleHandle() : counters{}, inner{&counters} {} +}; + +#define checkCounters() \ + do { \ + BOOST_REQUIRE_EQUAL(l.counters.nCopy, counters.nCopy); \ + BOOST_REQUIRE_EQUAL(l.counters.nCopyConstruct, counters.nCopyConstruct); \ + BOOST_REQUIRE_EQUAL(l.counters.nMove, counters.nMove); \ + BOOST_REQUIRE_EQUAL(l.counters.nMoveConstruct, counters.nMoveConstruct); \ + BOOST_REQUIRE_EQUAL(l.counters.nDestroy, counters.nDestroy); \ + } while (0) + +#define makeCounter(counter, n) \ + do { \ + counter += n; \ + checkCounters(); \ + } while (0) + +#define incCopyConstruct(n) makeCounter(counters.nCopyConstruct, n) +#define incCopy(n) makeCounter(counters.nCopy, n) +#define incMoveConstruct(n) makeCounter(counters.nMoveConstruct, n) +#define incMove(n) makeCounter(counters.nMove, n) +#define incDestroy(n) makeCounter(counters.nDestroy, n) + +BOOST_AUTO_TEST_CASE(LifeCycleSmall) { + LifecycleCounters counters; + LifecycleHandle<0> l; + + checkCounters(); + + { + const auto& o = l.inner; // force copy + Any a{o}; + incCopyConstruct(1); + + const auto& _a = a; // force copy later + { + Any b{_a}; + incCopyConstruct(1); + } + incDestroy(1); + + { + Any b; + b = _a; + incCopyConstruct(1); + b = _a; + incCopy(1); + } + incDestroy(1); + + { + Any b{a}; + incCopyConstruct(1); + b = a; + incCopy(1); + } + incDestroy(1); + + { + auto _a2 = a; + incCopyConstruct(1); + Any b; + b = std::move(_a2); + incMoveConstruct(1); + auto _a3 = a; + incCopyConstruct(1); + b = std::move(_a3); + incMove(1); + } + incDestroy(3); + } + incDestroy(1); + + checkCounters(); + + CHECK_ANY_ALLOCATIONS(); +} + +BOOST_AUTO_TEST_CASE(LifeCycleHeap) { + LifecycleCounters counters; + LifecycleHandle<512> l; + + checkCounters(); + + { + const auto& o = l.inner; // force copy + Any a{o}; + incCopyConstruct(1); + + const auto& _a = a; // force copy later + { + Any b{_a}; + incCopyConstruct(1); + } + incDestroy(1); + + { + Any b; + b = _a; + incCopyConstruct(1); + b = _a; + incCopy(1); + } + incDestroy(1); + + { + Any b{a}; + incCopyConstruct(1); + b = a; + incCopy(1); + } + incDestroy(1); + + { + Any _a2 = a; + incCopyConstruct(1); + Any b; + b = std::move(_a2); + // no actual move + + Any _a3 = a; + incCopyConstruct(1); + b = std::move(_a3); + // no actual move + incDestroy(1); + } + incDestroy(1); + } + incDestroy(1); + + checkCounters(); + + CHECK_ANY_ALLOCATIONS(); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/Tests/UnitTests/Core/Utilities/CMakeLists.txt b/Tests/UnitTests/Core/Utilities/CMakeLists.txt index 9c2efcc6b39..79e2a1a08e9 100644 --- a/Tests/UnitTests/Core/Utilities/CMakeLists.txt +++ b/Tests/UnitTests/Core/Utilities/CMakeLists.txt @@ -36,3 +36,7 @@ if (ACTS_BUILD_CUDA_FEATURES) add_unittest(Cuda CudaTests.cu) add_unittest(CudaMostSimplified CudaMostSimplifiedTests.cu) endif() + +add_unittest(Any AnyTests.cpp) +add_unittest(AnyDebug AnyTests.cpp) +target_compile_definitions(ActsUnitTestAnyDebug PRIVATE _ACTS_ANY_ENABLE_VERBOSE _ACTS_ANY_ENABLE_DEBUG _ACTS_ANY_ENABLE_TRACK_ALLOCATIONS)