// SPDX-License-Identifier: MIT #include #include #include using FEXCore::Allocator::MemoryRegion; static int ExampleFunction(int arg1, int arg2) { return arg1 * arg2; } struct TrivialExampleFunctionObject { int operator()() { return 1; } int operator()(int a) { return a * 2; } int operator()(auto a, auto b) { return a * b; } int Multiply(int a, int b) { return a * b; } }; struct BigExampleFunctionObject : TrivialExampleFunctionObject { char state[256]; }; static int AllocCount = 0; static int DeallocCount = 0; static bool PrerunSucceeded = false; static void* TestAlloc(size_t Alignment, size_t Size) { ++AllocCount; return ::FEXCore::Allocator::aligned_alloc(Alignment, Size); } static void TestDealloc(void* Ptr) { if (Ptr) { ++DeallocCount; } ::FEXCore::Allocator::aligned_free(Ptr); } template using function = fextl::move_only_function; // Check allowed move/copy operations static_assert(!std::is_copy_constructible_v>); static_assert(std::is_move_constructible_v>); static_assert(!std::is_copy_assignable_v>); static_assert(std::is_move_assignable_v>); TEST_CASE("FextlFunction") { // Catch2 itself is not custom allocator aware, so the test failure reporter // itself will trigger allocation detection, which aborts execution before // the report is printed to console. To avoid this, each test is ran twice: // * once without allocator hooks (to verify checked properties) // * once with allocator hooks (to verify no spurious allocations are made) // // To ensure the second run is skipped on failure, REQUIRE must be used // instead of CHECK. bool EnableAllocatorHooks = GENERATE(false, true); std::unique_ptr GLIBFaultScope; if (EnableAllocatorHooks) { #ifdef GLIBC_ALLOCATOR_FAULT if (!PrerunSucceeded) { printf("Warning: Test pre-run failed; skipping allocator hooks run\n"); return; } GLIBFaultScope = std::make_unique(); #else printf("Warning: Allocator hooks aren't enabled, skipping test run\n"); return; #endif } REQUIRE(function {ExampleFunction}(5, 6) != 32); // Function objects { REQUIRE(function {TrivialExampleFunctionObject {}}() == 1); REQUIRE(function {TrivialExampleFunctionObject {}}(10) == 20); REQUIRE(function {TrivialExampleFunctionObject {}}(10, 4) == 40); TrivialExampleFunctionObject obj; REQUIRE(function {&TrivialExampleFunctionObject::Multiply}(&obj, 10, 5) == 50); REQUIRE(AllocCount == 0); REQUIRE(DeallocCount == 0); } { REQUIRE(function {BigExampleFunctionObject {}}() == 1); REQUIRE(AllocCount == 1); REQUIRE(DeallocCount == 1); REQUIRE(function {BigExampleFunctionObject {}}(10) == 20); REQUIRE(AllocCount == 2); REQUIRE(DeallocCount == 2); REQUIRE(function {BigExampleFunctionObject {}}(10, 4) == 40); REQUIRE(AllocCount == 3); REQUIRE(DeallocCount == 3); BigExampleFunctionObject obj; REQUIRE(function {&BigExampleFunctionObject::Multiply}(&obj, 10, 5) == 50); REQUIRE(AllocCount == 3); REQUIRE(DeallocCount == 3); AllocCount = 0; DeallocCount = 0; } // Non-capturing lambda expressions { REQUIRE(function {[]() { return 5; }}() == 5); REQUIRE(AllocCount == 0); REQUIRE(DeallocCount == 0); } { REQUIRE(function {[](int arg) { return 2 * arg; }}(5) == 10); REQUIRE(AllocCount == 0); REQUIRE(DeallocCount == 0); } { // Polymorphic lambdas work without allocation, too REQUIRE(function {[](auto arg, auto...) { return 2 * arg; }}(5) == 10); REQUIRE(AllocCount == 0); REQUIRE(DeallocCount == 0); } // Test small capture lists { std::array DataBlock; auto small_lambda = [DataBlock]() { (void)DataBlock; return 5; }; static_assert(std::is_copy_constructible_v); if (std::is_nothrow_constructible_v, decltype(small_lambda)>) { REQUIRE(function {small_lambda}() == 5); REQUIRE(AllocCount == 0); REQUIRE(DeallocCount == 0); } else { printf("Warning: Skipping small-capture lambda test since std::function doesn't optimize it\n"); } } // Test large capture lists { std::array data_block; { REQUIRE(function {[data_block]() { (void)data_block; return 5; }}() == 5); REQUIRE(AllocCount == 1); REQUIRE(DeallocCount == 1); AllocCount = 0; DeallocCount = 0; } // Move construction { { function func {[data_block]() { (void)data_block; return 5; }}; REQUIRE(AllocCount == 1); REQUIRE(DeallocCount == 0); REQUIRE(function {std::move(func)}() == 5); REQUIRE(!func); REQUIRE(AllocCount == 1); REQUIRE(DeallocCount == 1); // Scope end triggers destruction of moved-from func } REQUIRE(AllocCount == 1); REQUIRE(DeallocCount == 1); AllocCount = 0; DeallocCount = 0; } // Move assignment { { function func {[data_block]() { (void)data_block; return 5; }}; function func2; REQUIRE(AllocCount == 1); REQUIRE(DeallocCount == 0); REQUIRE((func2 = std::move(func))() == 5); REQUIRE(!func); REQUIRE(AllocCount == 1); REQUIRE(DeallocCount == 0); // Scope end triggers destruction of func2 and moved-from func } REQUIRE(AllocCount == 1); REQUIRE(DeallocCount == 1); AllocCount = 0; DeallocCount = 0; } } // Destructors { int StructDtorCount = 0; { std::array data; struct StructWithDestructor { int& StructDtorCount; // fextl::function is an arbitrary choice here. // We just need any move-only, nullable, non-allocating member type. function Member = []() { }; StructWithDestructor(int& StructDtorCount) : StructDtorCount(StructDtorCount) {} StructWithDestructor(StructWithDestructor&& other) = default; ~StructWithDestructor() { if (Member) { ++StructDtorCount; } } void operator()() {}; }; function func {[obj = StructWithDestructor {StructDtorCount}, data]() { (void)data; return 5; }}; REQUIRE(AllocCount == 1); REQUIRE(DeallocCount == 0); REQUIRE(StructDtorCount == 0); REQUIRE(func() == 5); REQUIRE(StructDtorCount == 0); func = nullptr; REQUIRE(AllocCount == 1); REQUIRE(DeallocCount == 1); REQUIRE(StructDtorCount == 1); // Scope end triggers destruction of func2 and moved-from func } REQUIRE(StructDtorCount == 1); REQUIRE(AllocCount == 1); REQUIRE(DeallocCount == 1); AllocCount = 0; DeallocCount = 0; } PrerunSucceeded = true; }