diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 7221d90e8..46be97042 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -6,7 +6,7 @@ "label": "build", "command": "build", "targets": [ - "all" + "all", "Tests" ], "preset": "__defaultBuildPreset__", "group": "build", diff --git a/aui.audio/benchmarks/PlayBenchmark.cpp b/aui.audio/benchmarks/PlayBenchmark.cpp index 0eea5a248..95dd8af92 100644 --- a/aui.audio/benchmarks/PlayBenchmark.cpp +++ b/aui.audio/benchmarks/PlayBenchmark.cpp @@ -13,7 +13,7 @@ class GeneratedSoundStream: public ISoundInputStream { size_t read(char* dst, size_t size) override { auto span = std::span(reinterpret_cast(dst), size / sizeof(int16_t)); for (auto& s : span) { - s = std::sin(mTime += 0.03f) * 1000; + s = std::sin(mTime += 0.03f) * 10; } mTime = std::fmod(mTime, glm::pi() * 2.f); return span.size_bytes(); diff --git a/aui.core/benchmarks/SmallPimplBenchmark.cpp b/aui.core/benchmarks/SmallPimplBenchmark.cpp new file mode 100644 index 000000000..d91279c77 --- /dev/null +++ b/aui.core/benchmarks/SmallPimplBenchmark.cpp @@ -0,0 +1,133 @@ +// AUI Framework - Declarative UI toolkit for modern C++20 +// Copyright (C) 2020-2025 Alex2772 and Contributors +// +// SPDX-License-Identifier: MPL-2.0 +// +// 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 "AUI/Util/SmallPimpl.h" +#include "AUI/Util/Assert.h" +#include +#include + +// /home/alex2772/CLionProjects/aui/cmake-build-relwithdebinfo/bin/Benchmarks --benchmark_filter=.* +// 2026-02-20T02:56:27+03:00 +// Running /home/alex2772/CLionProjects/aui/cmake-build-relwithdebinfo/bin/Benchmarks +// Run on (32 X 5086.18 MHz CPU s) +// CPU Caches: +// L1 Data 32 KiB (x16) +// L1 Instruction 32 KiB (x16) +// L2 Unified 512 KiB (x16) +// L3 Unified 32768 KiB (x2) +// Load Average: 14.71, 6.43, 3.65 +// ***WARNING*** CPU scaling is enabled, the benchmark real time measurements may be noisy and will incur extra overhead. +// ***WARNING*** ASLR is enabled, the results may have unreproducible noise in them. +// ---------------------------------------------------------------------------------------------------------- +// Benchmark Time CPU Iterations +// ---------------------------------------------------------------------------------------------------------- +// SmallPimpl_Allocation, SmallImpl> 2.12 ns 2.11 ns 333858259 +// SmallPimpl_Allocation, SmallImpl> 8.59 ns 8.55 ns 83463874 +// SmallPimpl_Allocation, MediumImpl> 3.22 ns 3.20 ns 220340695 +// SmallPimpl_Allocation, MediumImpl> 8.36 ns 8.33 ns 82877739 +// SmallPimpl_Allocation, BigImpl> 51.1 ns 50.7 ns 14034323 +// SmallPimpl_Allocation, BigImpl> 52.6 ns 52.4 ns 12969600 +// SmallPimpl_Copy, SmallImpl> 4.24 ns 4.20 ns 166740471 +// SmallPimpl_Copy, SmallImpl> 10.2 ns 10.1 ns 70236187 +// SmallPimpl_Copy, MediumImpl> 4.22 ns 4.20 ns 166158461 +// SmallPimpl_Copy, MediumImpl> 10.7 ns 10.7 ns 69750309 +// SmallPimpl_Copy, BigImpl> 26.6 ns 26.5 ns 26582109 +// SmallPimpl_Copy, BigImpl> 26.0 ns 25.9 ns 27228219 +// SmallPimpl_Invoke, SmallImpl> 1.16 ns 1.15 ns 609427092 +// SmallPimpl_Invoke, SmallImpl> 1.20 ns 1.19 ns 598789144 +// SmallPimpl_Invoke, MediumImpl> 1.15 ns 1.15 ns 607650646 +// SmallPimpl_Invoke, MediumImpl> 1.39 ns 1.38 ns 508979367 +// SmallPimpl_Invoke, BigImpl> 1.44 ns 1.42 ns 511851310 +// SmallPimpl_Invoke, BigImpl> 1.17 ns 1.17 ns 597973846 + +using namespace std::chrono_literals; + +namespace { +struct ITest { + virtual ~ITest() = default; + virtual void test() = 0; + + void operator()() { + this->test(); + } +}; + +// Small implementation (fits in stack buffer) +struct SmallImpl final: ITest { + void test() override {} +}; + +// Large implementation (forces heap allocation for std::function only) +struct MediumImpl final: ITest { + char padding[64]; + void test() override {} +}; + +// Large implementation (forces heap allocation) +struct BigImpl final: ITest { + char padding[1024]; + void test() override {} +}; +} + +template +static void SmallPimpl_Allocation(benchmark::State& state) { + for (auto _ : state) { + Pimpl pimpl{Impl{}}; + benchmark::DoNotOptimize(pimpl); + } +} + +template +static void SmallPimpl_Copy(benchmark::State& state) { + Pimpl pimpl1{Impl{}}; + for (auto _ : state) { + Pimpl pimpl2{pimpl1}; + benchmark::DoNotOptimize(pimpl2); + } +} + +void invoke(std::function& t) { + std::invoke(t); +} + +void invoke(aui::small_pimpl& t) { + t->test(); +} + +template +static void SmallPimpl_Invoke(benchmark::State& state) { + Pimpl pimpl1{Impl{}}; + for (auto _ : state) { + invoke(pimpl1); + } +} + + + +// std::function was added for comparison +BENCHMARK(SmallPimpl_Allocation, SmallImpl>); +BENCHMARK(SmallPimpl_Allocation, SmallImpl>); +BENCHMARK(SmallPimpl_Allocation, MediumImpl>); +BENCHMARK(SmallPimpl_Allocation, MediumImpl>); +BENCHMARK(SmallPimpl_Allocation, BigImpl>); +BENCHMARK(SmallPimpl_Allocation, BigImpl>); +BENCHMARK(SmallPimpl_Copy, SmallImpl>); +BENCHMARK(SmallPimpl_Copy, SmallImpl>); +BENCHMARK(SmallPimpl_Copy, MediumImpl>); +BENCHMARK(SmallPimpl_Copy, MediumImpl>); +BENCHMARK(SmallPimpl_Copy, BigImpl>); +BENCHMARK(SmallPimpl_Copy, BigImpl>); +BENCHMARK(SmallPimpl_Invoke, SmallImpl>); +BENCHMARK(SmallPimpl_Invoke, SmallImpl>); +BENCHMARK(SmallPimpl_Invoke, MediumImpl>); +BENCHMARK(SmallPimpl_Invoke, MediumImpl>); +BENCHMARK(SmallPimpl_Invoke, BigImpl>); +BENCHMARK(SmallPimpl_Invoke, BigImpl>); diff --git a/aui.core/src/AUI/Util/APimpl.h b/aui.core/src/AUI/Util/APimpl.h index 3fb01938b..3f61c3ecf 100644 --- a/aui.core/src/AUI/Util/APimpl.h +++ b/aui.core/src/AUI/Util/APimpl.h @@ -20,11 +20,10 @@ namespace aui { * @brief Utility wrapper implementing the stack-allocated (fast) pimpl idiom. * @ingroup useful_templates * @details - * the following functions can be called only if T is a complete type: - *
    - *
  • ctor
  • - *
  • dtor
  • - *
+ * the following functions can be called only if `T` is a complete type: + * + * - ctor + * - dtor * * See https://youtu.be/mkPTreWiglk?t=157 (Russian) */ diff --git a/aui.core/src/AUI/Util/SmallPimpl.h b/aui.core/src/AUI/Util/SmallPimpl.h new file mode 100644 index 000000000..02f466364 --- /dev/null +++ b/aui.core/src/AUI/Util/SmallPimpl.h @@ -0,0 +1,318 @@ +/* + * AUI Framework - Declarative UI toolkit for modern C++20 + * Copyright (C) 2020-2026 Alex2772 and Contributors + * + * SPDX-License-Identifier: MPL-2.0 + * + * 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 "AUI/Common/AException.h" +#include "AUI/Traits/unsafe_declval.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace aui { + +/** + * @brief Small buffer optimized PIMPL implementation. + * @ingroup useful_templates + * @details + * `small_pimpl` behaves similarly to `std::unique_ptr` but stores the + * implementation object inline when its size is less than or equal to + * `StackSize`. When the object is larger, a heap allocation is used. + * + * The class provides a pointer‑like interface to the underlying + * implementation, supporting construction, copy/move semantics, and + * destruction. It is useful for the pImpl idiom where the concrete type + * may be small enough to avoid dynamic allocation. + * + * It is is a good compromise for most small objects and matches the size used by `boost::function` or `std::any`. + * + * `T` must implement interface `Interface`. + * + * If `small_pimpl` is holding a non-move-constructible object, it will attempt to use copy constructor instead. + * + * If `small_pimpl` is holding a non-copy-constructible object, `small_pimpl` will throw an exception in runtime on a + * copy attempt. + * + * @tparam Interface The public interface type (must be a class). + * @tparam StackSize Size of the stack buffer in bytes. + */ +template +class small_pimpl { + static_assert(std::is_class_v, "Interface must be a class type"); + static_assert(std::has_virtual_destructor_v, "Interface must have virtual destructor"); + static_assert(StackSize > 0, "StackSize must be greater than zero"); + + // ==== NOTES ON IMPLEMENTATION === + // + // Interface* and T* might mismatch. Hence, we need to keep track of both pointers. + // both StackAllocated and HeapAllocated provide: + // - ptr(), which is a valid `Interface*`. This ptr() is facing to the user of small_pimpl. + // - upcastedPtr(), which is a valid `T*`. The pointer is type erasured to void*. These pointers are dispatched by + // corresponding control blocks, whose pointer is stored as well. + // + // Ownership and destructors are dispatched by std::variant. + +private: + /** + * @brief Handles upcasted pointers. Implemented and stored in getControlBlock(). + */ + struct ControlBlock { + /** + * @brief Translates upcasted pointer (T*) `ptr` to `Interface*`. + * @param ptr object of type T. + */ + virtual Interface* asInterface(void* ptr) const = 0; + + /** + * @brief Calls copy inplace constructor within `dst`. + * @param dst destination buffer. + * @param src source object of type T. + */ + virtual void copyInplace(void* dst, void* src) const = 0; + + /** + * @brief Calls a copy constructor on `src`. Returns unique_ptr and upcasted pointers to newly created object. + * @param src source object of type T. + */ + virtual std::tuple, void* /* upcastedPtr */> copy(void* src) const = 0; + + /** + * @brief Calls copy inplace constructor within `dst`. Fallbacks to copyInplace if move ctor is not available. + * @param dst destination buffer. + * @param src source object of type T. + */ + virtual void moveInplace(void* dst, void* src) const = 0; + }; + + struct Common { + Interface* ptr{}; + const ControlBlock* controlBlock{}; + }; + + struct Empty: Common { + // stub container, needed just to avoid the requirement of initializing mStorage in constructor immediately. + }; + + struct StackAllocated: Common { + alignas(Interface) unsigned char buffer[StackSize]; + + template + StackAllocated(const ControlBlock& controlBlock, std::in_place_type_t, Args&&... args) { + this->ptr = new (&buffer) std::decay_t(std::forward(args)...); + this->controlBlock = &controlBlock; + } + + StackAllocated(const StackAllocated& rhs) { + this->controlBlock = rhs.controlBlock; + this->controlBlock->copyInplace(upcastedPtr(), rhs.upcastedPtr()); + this->ptr = this->controlBlock->asInterface(upcastedPtr()); + } + + StackAllocated(StackAllocated&& rhs) noexcept { + this->controlBlock = rhs.controlBlock; + this->controlBlock->moveInplace(upcastedPtr(), rhs.upcastedPtr()); + this->ptr = this->controlBlock->asInterface(upcastedPtr()); + } + + ~StackAllocated() { this->ptr->~Interface(); } + + [[nodiscard]] + void* upcastedPtr() const noexcept { + return const_cast(static_cast(&buffer)); + } + }; + + struct HeapAllocated: Common { + std::unique_ptr value {}; + void* upcastedPtrValue {}; + + template + HeapAllocated(const ControlBlock& controlBlock, std::in_place_type_t, Args&&... args) { + this->controlBlock = &controlBlock; + // don't mess up with the order and types: std::unique_ptr IS NOT std::unique_ptr + // which are not required to be equal + auto object = std::make_unique(std::forward(args)...); + upcastedPtrValue = this->ptr = object.get(); + value = std::move(object); + } + + HeapAllocated(const HeapAllocated& rhs) { + this->controlBlock = rhs.controlBlock; + auto [uniquePtr, upcastedPtr] = this->controlBlock->copy(rhs.upcastedPtrValue); + value = std::move(uniquePtr); + this->ptr = value.get(); + upcastedPtrValue = upcastedPtr; + } + + HeapAllocated(HeapAllocated&& rhs) noexcept + : value(std::move(rhs.value)) + , upcastedPtrValue(std::exchange(rhs.upcastedPtrValue, nullptr)) { + this->controlBlock = rhs.controlBlock; + this->ptr = value.get(); + } + + ~HeapAllocated() = default; // no need to call dtor ourselves, unique_ptr does it for us + + [[nodiscard]] + void* upcastedPtr() const noexcept { + return upcastedPtrValue; + } + }; + + using Storage = std::variant; + +public: + /** + * @brief Construct from a concrete type. + * @tparam T Concrete type that implements `Interface`. + * @details + * If `T` is neither copy-constructible nor move-constructible, you can use inplace constructor of `small_pimpl` + * only. In such a scenario, you are likely to get a runtime error when attempting to copy/move a `small_pimpl`. + */ + template T> + explicit small_pimpl(T t) : small_pimpl(std::in_place_type, std::forward(t)) { + static_assert( + std::is_copy_constructible_v || std::is_move_constructible_v, + "T must be either copy or move constructible; you can use inplace construction instead."); + } + + /** + * @brief In place construct from concrete type. + * @tparam T Concrete type that implements `Interface`. + * @tparam Args Constructor arguments. + */ + template T, typename... Args> + explicit small_pimpl(std::in_place_type_t, Args&&... args) { + static_assert(std::is_base_of_v, "T must derive from Interface"); + static_assert(!std::is_abstract_v, "T must not be an abstract type"); + static_assert(std::is_constructible_v, "T must be constructible from args"); + + const auto& controlBlock = CONTROL_BLOCK_IMPL>; + + if constexpr (isStackConstructible()) { + mStorage.template emplace(controlBlock, std::in_place_type, std::forward(args)...); + } else { + mStorage.template emplace(controlBlock, std::in_place_type, std::forward(args)...); + } + } + + // std::variant will figure out this shit for us + explicit small_pimpl(const small_pimpl& rhs) = default; + small_pimpl& operator=(const small_pimpl& rhs) = default; + explicit small_pimpl(small_pimpl&& rhs) noexcept : mStorage(std::exchange(rhs.mStorage, Empty {})) {} + small_pimpl& operator=(small_pimpl&& rhs) noexcept { + if (this == &rhs) + return *this; + mStorage = std::exchange(rhs.mStorage, Empty {}); + return *this; + } + + static_assert(std::is_copy_constructible_v); + static_assert(std::is_move_constructible_v); + + /** + * @brief Access the interface. + */ + [[nodiscard]] Interface* operator->() noexcept { return ptr(); } + [[nodiscard]] const Interface* operator->() const noexcept { return ptr(); } + + /** + * @brief Dereference. + */ + [[nodiscard]] Interface& operator*() noexcept { return *ptr(); } + [[nodiscard]] const Interface& operator*() const noexcept { return *ptr(); } + + /** + * @brief Get the stored pointer to interface. + */ + [[nodiscard]] Interface* ptr() noexcept { + // std::visit dispatch is stripped down by compiler optimizations. + return std::visit([](Common& p) { return p.ptr; }, mStorage); + } + + [[nodiscard]] const Interface* ptr() const noexcept { + // std::visit dispatch is stripped down by compiler optimizations. + return std::visit([](const Common& p) { return p.ptr; }, mStorage); + } + + [[nodiscard]] const ControlBlock& controlBlock() const noexcept { + // std::visit dispatch is stripped down by compiler optimizations. + return std::visit([](const Common& p) -> decltype(auto) { return *p.controlBlock; }, mStorage); + } + + [[nodiscard]] + bool isOnStack() const noexcept { + return std::holds_alternative(mStorage); + } + + [[nodiscard]] + bool isOnHeap() const noexcept { + return std::holds_alternative(mStorage); + } + +private: + Storage mStorage = Empty {}; + + template T> + struct ControlBlockImpl final : ControlBlock { + Interface* asInterface(void* ptr) const override { return static_cast(ptr); } + + void copyInplace(void* dst, void* src) const override { + const auto& srcT = *static_cast(src); + if constexpr (std::is_copy_constructible_v) { + new (dst) T(srcT); + } else { + throw AException( + "small_pimpl: attempt to make a copy or move of small_pimpl: {} is not copy constructible"_format( + AReflect::name(static_cast(src)))); + } + } + + std::tuple, void* /* upcastedPtr */> copy(void* src) const override { + const auto& srcT = *static_cast(src); + if constexpr (std::is_copy_constructible_v) { + auto copy = std::make_unique(srcT); + auto* upcastedPtr = copy.get(); + return { std::move(copy), upcastedPtr }; + } else { + throw AException( + "small_pimpl: attempt to make a copy or move of small_pimpl: {} is not copy constructible"_format( + AReflect::name(static_cast(src)))); + } + } + + void moveInplace(void* dst, void* src) const override { + auto& srcT = *static_cast(src); + if constexpr (std::is_move_constructible_v) { + new (dst) T(std::move(srcT)); + } else { + copyInplace(dst, src); + } + } + + }; + + template + static constexpr ControlBlockImpl CONTROL_BLOCK_IMPL{}; + + template + static consteval bool isStackConstructible() { + return sizeof(T) <= StackSize && alignof(T) <= alignof(Interface); + } +}; + +} // namespace aui diff --git a/aui.core/tests/SmallPimplTest.cpp b/aui.core/tests/SmallPimplTest.cpp new file mode 100644 index 000000000..af97775bd --- /dev/null +++ b/aui.core/tests/SmallPimplTest.cpp @@ -0,0 +1,196 @@ +// AUI Framework - Declarative UI toolkit for modern C++20 +// Copyright (C) 2020-2025 Alex2772 and Contributors +// +// SPDX-License-Identifier: MPL-2.0 +// +// 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 "AUI/Util/SmallPimpl.h" + +#include "AUI/Image/APixelFormat.h" + +// Define a simple interface used by the small_pimpl tests. +// It contains a single virtual method `test()` that will be +// overridden by mock implementations. +struct MyInterface { + int field = 0; + virtual ~MyInterface() = default; + virtual void test() = 0; +}; + +thread_local bool destructorCalled = false; + +// implementation that is intentionally small enough to be +// stored on the stack by `small_pimpl`. +struct small { + // The test expects this mock to be allocated on the stack. + static constexpr bool EXPECT_HEAP_ALLOCATED = false; + class Mock : public MyInterface { + public: + static constexpr bool EXPECT_HEAP_ALLOCATED = false; + MOCK_METHOD(void, test, (), (override)); + + ~Mock() override { + destructorCalled = true; + } + }; + + class CopyOnly : public MyInterface { + public: + CopyOnly() = default; + CopyOnly(const CopyOnly&) = default; + CopyOnly(CopyOnly&&) = delete; + + void test() override {} + ~CopyOnly() override { + destructorCalled = true; + } + }; + + class MoveOnly : public MyInterface { + public: + MoveOnly() = default; + MoveOnly(const MoveOnly&) = delete; + MoveOnly(MoveOnly&&) = default; + + void test() override {} + ~MoveOnly() override { + destructorCalled = true; + } + }; +}; + + +// implementation that is intentionally large, forcing +// `small_pimpl` to allocate it on the heap. +struct big { + // The test expects this mock to be allocated on the heap. + static constexpr bool EXPECT_HEAP_ALLOCATED = true; + struct Mock : public MyInterface { + char padding[1024]; + MOCK_METHOD(void, test, (), (override)); + + ~Mock() override { + destructorCalled = true; + } + }; + class CopyOnly : public MyInterface { + public: + char padding[1024]; + CopyOnly() = default; + CopyOnly(const CopyOnly&) = default; + CopyOnly(CopyOnly&&) = delete; + + void test() override {} + ~CopyOnly() override { + destructorCalled = true; + } + }; + + class MoveOnly : public MyInterface { + public: + char padding[1024]; + MoveOnly() = default; + MoveOnly(const MoveOnly&) = delete; + MoveOnly(MoveOnly&&) = default; + + void test() override {} + ~MoveOnly() override { + destructorCalled = true; + } + }; +}; + +static constexpr std::size_t SMALL_SIZE = 128; +static_assert(sizeof(small::Mock) <= SMALL_SIZE); +static_assert(sizeof(small::CopyOnly) <= SMALL_SIZE); +static_assert(sizeof(small::MoveOnly) <= SMALL_SIZE); +static_assert(sizeof(big::Mock) > SMALL_SIZE); +static_assert(sizeof(big::CopyOnly) > SMALL_SIZE); +static_assert(sizeof(big::MoveOnly) > SMALL_SIZE); + +using MyPimpl = aui::small_pimpl; + +// Test fixture template that will be instantiated with each mock type. +template +class SmallPimpl : public testing::Test {}; + +using TestTypes = testing::Types; +TYPED_TEST_SUITE(SmallPimpl, TestTypes); + +// Verify that `small_pimpl` chooses the correct storage location +// (stack vs heap) based on the size of the concrete type. +TYPED_TEST(SmallPimpl, AllocationDecision) { + using MockInterface = TypeParam::Mock; // either small::Mock or big::Mock + + MyPimpl pimpl{std::in_place_type}; + EXPECT_EQ(pimpl.isOnHeap(), TypeParam::EXPECT_HEAP_ALLOCATED); +} + +// Test that the mock's `test()` method is correctly invoked through +// the `small_pimpl` interface. +TYPED_TEST(SmallPimpl, MockTestInvocation) { + using MockInterface = TypeParam::Mock; // either small::Mock or big::Mock + + MyPimpl pimpl{std::in_place_type}; + { + auto* mock = dynamic_cast(pimpl.ptr()); + ASSERT_NE(mock, nullptr); + EXPECT_CALL(*mock, test()).Times(1); + } + pimpl->test(); +} + +// Ensure that the destructor of the concrete type is called when +// the `small_pimpl` goes out of scope. +TYPED_TEST(SmallPimpl, Destructor) { + using MockInterface = TypeParam::Mock; // either small::Mock or big::Mock + + { + MyPimpl pimpl{std::in_place_type}; + destructorCalled = false; + } + EXPECT_TRUE(destructorCalled); +} + +TYPED_TEST(SmallPimpl, Copy) { + using CopyOnly = TypeParam::CopyOnly; // either small::CopyOnly or big::CopyOnly + + { + MyPimpl pimpl1{std::in_place_type}; + pimpl1->field = 42; + { + MyPimpl pimpl2{pimpl1}; + EXPECT_EQ(pimpl2->field, 42); + destructorCalled = false; + } + EXPECT_TRUE(destructorCalled); + destructorCalled = false; + } + EXPECT_TRUE(destructorCalled); +} + +TYPED_TEST(SmallPimpl, Move) { + using MoveOnly = TypeParam::MoveOnly; // either small::MoveOnly or big::MoveOnly + + { + MyPimpl pimpl1{std::in_place_type}; + pimpl1->field = 42; + { + EXPECT_ANY_THROW(MyPimpl{pimpl1}); + MyPimpl pimpl2{std::move(pimpl1)}; + EXPECT_EQ(pimpl2->field, 42); + destructorCalled = false; + } + EXPECT_TRUE(destructorCalled); + destructorCalled = false; + } + EXPECT_FALSE(destructorCalled); +} + + +