Loading media/utils/include/mediautils/InPlaceFunction.h 0 → 100644 +267 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * 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 <cstdlib> #include <functional> #include <memory> #include <type_traits> namespace android::mediautils { namespace detail { // Vtable interface for erased types template <typename Ret, typename... Args> struct ICallableTable { // Destroy the erased type void (*destroy)(void* storage) = nullptr; // Call the erased object Ret (*invoke)(void* storage, Args...) = nullptr; // **Note** the next two functions only copy object data, not the vptr // Copy the erased object to a new InPlaceFunction buffer void (*copy_to)(const void* storage, void* other) = nullptr; // Move the erased object to a new InPlaceFunction buffer void (*move_to)(void* storage, void* other) = nullptr; }; } // namespace detail // This class is an *almost* drop-in replacement for std::function which is guaranteed to never // allocate, and always holds the type erased functional object in an in-line small buffer of // templated size. If the object is too large to hold, the type will fail to instantiate. // // Two notable differences are: // - operator() is not const (unlike std::function where the call operator is // const even if the erased type is not const callable). This retains const // correctness by default. A workaround is keeping InPlaceFunction mutable. // - Moving from an InPlaceFunction leaves the object in a valid state (operator // bool remains true), similar to std::optional/std::variant. // Calls to the object are still defined (and are equivalent // to calling the underlying type after it has been moved from). To opt-out // (and/or ensure safety), clearing the object is recommended: // func1 = std::move(func2); // func2 still valid (and moved-from) after this line // func2 = nullptr; // calling func2 will now abort template <typename, size_t BufferSize = 32> class InPlaceFunction; // We partially specialize to match types which are spelled like functions template <typename Ret, typename... Args, size_t BufferSize> class InPlaceFunction<Ret(Args...), BufferSize> { public: // Storage Type Details static constexpr size_t Size = BufferSize; static constexpr size_t Alignment = alignof(std::max_align_t); using Buffer_t = std::aligned_storage_t<Size, Alignment>; template <typename T, size_t Other> friend class InPlaceFunction; private: // Callable which is used for empty InPlaceFunction objects (to match the // std::function interface). struct BadCallable { [[noreturn]] Ret operator()(Args...) { std::abort(); } }; static_assert(std::is_trivially_destructible_v<BadCallable>); // Implementation of vtable interface for erased types. // Contains only static vtable instantiated once for each erased type and // static helpers. template <typename T> struct TableImpl { // T should be a decayed type static_assert(std::is_same_v<T, std::decay_t<T>>); // Helper functions to get an unerased reference to the type held in the // buffer. std::launder is required to avoid strict aliasing rules. // The cast is always defined, as a precondition for these calls is that // (exactly) a T was placement new constructed into the buffer. constexpr static T& getRef(void* storage) { return *std::launder(reinterpret_cast<T*>(storage)); } constexpr static const T& getRef(const void* storage) { return *std::launder(reinterpret_cast<const T*>(storage)); } // Constexpr implies inline constexpr static detail::ICallableTable<Ret, Args...> table = { // Stateless lambdas are convertible to function ptrs .destroy = [](void* storage) { getRef(storage).~T(); }, .invoke = [](void* storage, Args... args) -> Ret { return std::invoke(getRef(storage), args...); }, .copy_to = [](const void* storage, void* other) { ::new (other) T(getRef(storage)); }, .move_to = [](void* storage, void* other) { ::new (other) T(std::move(getRef(storage))); }, }; }; // Check size/align requirements for the T in Buffer_t. We use a templated // struct to enable std::conjunction (see below). template <typename T> struct WillFit : std::integral_constant<bool, sizeof(T) <= Size && alignof(T) <= Alignment> {}; // Check size/align requirements for a function to function conversion template <typename T> struct ConversionWillFit : std::integral_constant<bool, (T::Size < Size) && (T::Alignment <= Alignment)> {}; template <typename T> struct IsInPlaceFunction : std::false_type {}; template <size_t BufferSize_> struct IsInPlaceFunction<InPlaceFunction<Ret(Args...), BufferSize_>> : std::true_type {}; // Pred is true iff T is a valid type to construct an InPlaceFunction with // We use std::conjunction for readability and short-circuit behavior // (checks are ordered). // The actual target type is the decay of T. template <typename T> static constexpr bool Pred = std::conjunction_v< std::negation<IsInPlaceFunction<std::decay_t<T>>>, // T is not also an InPlaceFunction // of the same signature. std::is_invocable_r<Ret, std::decay_t<T>, Args...>, // correct signature callable WillFit<std::decay_t<T>> // The target type fits in local storage >; template <typename T> static constexpr bool ConvertibleFunc = std::conjunction_v<IsInPlaceFunction<std::decay_t<T>>, // implies correctly invokable ConversionWillFit<std::decay_t<T>>>; // Members below // This must come first for alignment Buffer_t storage_; const detail::ICallableTable<Ret, Args...>* vptr_; constexpr void copy_to(InPlaceFunction& other) const { vptr_->copy_to(std::addressof(storage_), std::addressof(other.storage_)); other.vptr_ = vptr_; } constexpr void move_to(InPlaceFunction& other) { vptr_->move_to(std::addressof(storage_), std::addressof(other.storage_)); other.vptr_ = vptr_; } constexpr void destroy() { vptr_->destroy(std::addressof(storage_)); } template <typename T, typename Target = std::decay_t<T>> constexpr void genericInit(T&& t) { vptr_ = &TableImpl<Target>::table; ::new (std::addressof(storage_)) Target(std::forward<T>(t)); } template <typename T, typename Target = std::decay_t<T>> constexpr void convertingInit(T&& smallerFunc) { // Redundant, but just in-case static_assert(Target::Size < Size && Target::Alignment <= Alignment); if constexpr (std::is_lvalue_reference_v<T>) { smallerFunc.vptr_->copy_to(std::addressof(smallerFunc.storage_), std::addressof(storage_)); } else { smallerFunc.vptr_->move_to(std::addressof(smallerFunc.storage_), std::addressof(storage_)); } vptr_ = smallerFunc.vptr_; } public: // Public interface template <typename T, std::enable_if_t<Pred<T>>* = nullptr> constexpr InPlaceFunction(T&& t) { genericInit(std::forward<T>(t)); } // Conversion from smaller functions. template <typename T, std::enable_if_t<ConvertibleFunc<T>>* = nullptr> constexpr InPlaceFunction(T&& t) { convertingInit(std::forward<T>(t)); } constexpr InPlaceFunction(const InPlaceFunction& other) { other.copy_to(*this); } constexpr InPlaceFunction(InPlaceFunction&& other) { other.move_to(*this); } // Making functions default constructible has pros and cons, we will align // with the standard constexpr InPlaceFunction() : InPlaceFunction(BadCallable{}) {} constexpr InPlaceFunction(std::nullptr_t) : InPlaceFunction(BadCallable{}) {} #if __cplusplus >= 202002L constexpr #endif ~InPlaceFunction() { destroy(); } // The std::function call operator is marked const, but this violates const // correctness. We deviate from the standard and do not mark the operator as // const. Collections of InPlaceFunctions should probably be mutable. constexpr Ret operator()(Args... args) { return vptr_->invoke(std::addressof(storage_), args...); } constexpr InPlaceFunction& operator=(const InPlaceFunction& other) { if (std::addressof(other) == this) return *this; destroy(); other.copy_to(*this); return *this; } constexpr InPlaceFunction& operator=(InPlaceFunction&& other) { if (std::addressof(other) == this) return *this; destroy(); other.move_to(*this); return *this; } template <typename T, std::enable_if_t<Pred<T>>* = nullptr> constexpr InPlaceFunction& operator=(T&& t) { // We can't assign to ourselves, since T is a different type destroy(); genericInit(std::forward<T>(t)); return *this; } // Explicitly defining this function saves a move/dtor template <typename T, std::enable_if_t<ConvertibleFunc<T>>* = nullptr> constexpr InPlaceFunction& operator=(T&& t) { // We can't assign to ourselves, since T is different type destroy(); convertingInit(std::forward<T>(t)); return *this; } constexpr InPlaceFunction& operator=(std::nullptr_t) { return operator=(BadCallable{}); } // Moved from InPlaceFunctions are still considered valid (similar to // std::optional). If using std::move on a function object explicitly, it is // recommended that the object is reset using nullptr. constexpr explicit operator bool() const { return vptr_ != &TableImpl<BadCallable>::table; } constexpr void swap(InPlaceFunction& other) { if (std::addressof(other) == this) return; InPlaceFunction tmp{std::move(other)}; other.destroy(); move_to(other); destroy(); tmp.move_to(*this); } friend constexpr void swap(InPlaceFunction& lhs, InPlaceFunction& rhs) { lhs.swap(rhs); } }; } // namespace android::mediautils media/utils/tests/Android.bp +10 −0 Original line number Diff line number Diff line Loading @@ -192,3 +192,13 @@ cc_test { "extended_accumulator_tests.cpp", ], } cc_test { name: "inplace_function_tests", defaults: ["libmediautils_tests_defaults"], srcs: [ "inplace_function_tests.cpp" ], } media/utils/tests/inplace_function_tests.cpp 0 → 100644 +346 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * 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. */ #define LOG_TAG "inplace_function_tests" #include <mediautils/InPlaceFunction.h> #include <type_traits> #include <gtest/gtest.h> #include <log/log.h> using namespace android; using namespace android::mediautils; struct BigCallable { BigCallable(size_t* x, size_t val1, size_t val2) : ptr(x), a(val1), b(val2) {} size_t* ptr; size_t a; size_t b; size_t operator()(size_t input) const { *ptr += a * 100 + b * 10 + input; return 8; } }; TEST(InPlaceFunctionTests, Basic) { size_t x = 5; InPlaceFunction<size_t(size_t)> func; { BigCallable test{&x, 2, 3}; func = test; } EXPECT_EQ(func(2), 8ull); EXPECT_EQ(x, 232ull + 5); } TEST(InPlaceFunctionTests, Invalid) { InPlaceFunction<size_t(size_t)> func; EXPECT_TRUE(!func); InPlaceFunction<size_t(size_t)> func2{nullptr}; EXPECT_TRUE(!func2); InPlaceFunction<size_t(size_t)> func3 = [](size_t x) { return x; }; EXPECT_TRUE(!(!func3)); func3 = nullptr; EXPECT_TRUE(!func3); } TEST(InPlaceFunctionTests, MultiArg) { InPlaceFunction<size_t(size_t, size_t, size_t)> func = [](size_t a, size_t b, size_t c) { return a + b + c; }; EXPECT_EQ(func(2, 3, 5), 2ull + 3 + 5); } struct Record { Record(size_t m, size_t c, size_t d) : move_called(m), copy_called(c), dtor_called(d) {} Record() {} size_t move_called = 0; size_t copy_called = 0; size_t dtor_called = 0; friend std::ostream& operator<<(std::ostream& os, const Record& record) { return os << "Record, moves: " << record.move_called << ", copies: " << record.copy_called << ", dtor: " << record.dtor_called << '\n'; } }; bool operator==(const Record& lhs, const Record& rhs) { return lhs.move_called == rhs.move_called && lhs.copy_called == rhs.copy_called && lhs.dtor_called == rhs.dtor_called; } struct Noisy { Record& ref; size_t state; Noisy(Record& record, size_t val) : ref(record), state(val) {} Noisy(const Noisy& other) : ref(other.ref), state(other.state) { ref.copy_called++; } Noisy(Noisy&& other) : ref(other.ref), state(other.state) { ref.move_called++; } ~Noisy() { ref.dtor_called++; } size_t operator()() { return state; } }; TEST(InPlaceFunctionTests, CtorForwarding) { Record record; Noisy noisy{record, 17}; InPlaceFunction<size_t()> func{noisy}; EXPECT_EQ(record, Record(0, 1, 0)); // move, copy, dtor EXPECT_EQ(func(), 17ull); Record record2; Noisy noisy2{record2, 13}; InPlaceFunction<size_t()> func2{std::move(noisy2)}; EXPECT_EQ(record2, Record(1, 0, 0)); // move, copy, dtor EXPECT_EQ(func2(), 13ull); } TEST(InPlaceFunctionTests, FunctionCtorForwarding) { { Record record; Noisy noisy{record, 17}; InPlaceFunction<size_t()> func{noisy}; EXPECT_EQ(record, Record(0, 1, 0)); // move, copy, dtor EXPECT_EQ(func(), 17ull); InPlaceFunction<size_t()> func2{func}; EXPECT_EQ(record, Record(0, 2, 0)); // move, copy, dtor EXPECT_EQ(func2(), 17ull); } Record record; Noisy noisy{record, 13}; InPlaceFunction<size_t()> func{noisy}; EXPECT_EQ(record, Record(0, 1, 0)); // move, copy, dtor EXPECT_EQ(func(), 13ull); InPlaceFunction<size_t()> func2{std::move(func)}; EXPECT_EQ(record, Record(1, 1, 0)); // move, copy, dtor EXPECT_EQ(func2(), 13ull); // We expect moved from functions to still be valid EXPECT_TRUE(!(!func)); EXPECT_EQ(static_cast<bool>(func), static_cast<bool>(func2)); EXPECT_EQ(func(), 13ull); } TEST(InPlaceFunctionTests, Dtor) { Record record; { InPlaceFunction<size_t()> func; { Noisy noisy{record, 17}; func = noisy; } EXPECT_EQ(func(), 17ull); EXPECT_EQ(record.dtor_called, 1ull); } EXPECT_EQ(record.dtor_called, 2ull); } TEST(InPlaceFunctionTests, Assignment) { { Record record; Record record2; Noisy noisy{record, 17}; Noisy noisy2{record2, 5}; InPlaceFunction<size_t()> func{noisy}; EXPECT_EQ(func(), 17ull); EXPECT_EQ(record.dtor_called, 0ull); func = noisy2; EXPECT_EQ(record.dtor_called, 1ull); EXPECT_EQ(record2, Record(0, 1, 0)); // move, copy, dtor EXPECT_EQ(func(), 5ull); } { Record record; Record record2; Noisy noisy{record, 17}; Noisy noisy2{record2, 5}; InPlaceFunction<size_t()> func{noisy}; EXPECT_EQ(func(), 17ull); EXPECT_EQ(record.dtor_called, 0ull); func = std::move(noisy2); EXPECT_EQ(record.dtor_called, 1ull); EXPECT_EQ(record2, Record(1, 0, 0)); // move, copy, dtor EXPECT_EQ(func(), 5ull); } { Record record; Record record2; Noisy noisy{record, 17}; Noisy noisy2{record2, 13}; { InPlaceFunction<size_t()> func{noisy}; EXPECT_EQ(func(), 17ull); InPlaceFunction<size_t()> func2{noisy2}; EXPECT_EQ(record2, Record(0, 1, 0)); // move, copy, dtor EXPECT_EQ(record.dtor_called, 0ull); func = func2; EXPECT_EQ(record.dtor_called, 1ull); EXPECT_EQ(func(), 13ull); EXPECT_EQ(record2, Record(0, 2, 0)); // move, copy, dtor EXPECT_TRUE(static_cast<bool>(func2)); EXPECT_EQ(func2(), 13ull); } EXPECT_EQ(record2, Record(0, 2, 2)); // move, copy, dtor } { Record record; Record record2; Noisy noisy{record, 17}; Noisy noisy2{record2, 13}; { InPlaceFunction<size_t()> func{noisy}; EXPECT_EQ(func(), 17ull); InPlaceFunction<size_t()> func2{noisy2}; EXPECT_EQ(record.dtor_called, 0ull); EXPECT_EQ(record2, Record(0, 1, 0)); // move, copy, dtor func = std::move(func2); EXPECT_EQ(record.dtor_called, 1ull); EXPECT_EQ(func(), 13ull); EXPECT_EQ(record2, Record(1, 1, 0)); // move, copy, dtor // Moved from function is still valid EXPECT_TRUE(static_cast<bool>(func2)); EXPECT_EQ(func2(), 13ull); } EXPECT_EQ(record2, Record(1, 1, 2)); // move, copy, dtor } } TEST(InPlaceFunctionTests, Swap) { Record record1; Record record2; InPlaceFunction<size_t()> func1 = Noisy{record1, 5}; InPlaceFunction<size_t()> func2 = Noisy{record2, 7}; EXPECT_EQ(record1, Record(1, 0, 1)); // move, copy, dtor EXPECT_EQ(record2, Record(1, 0, 1)); // move, copy, dtor EXPECT_EQ(func1(), 5ull); EXPECT_EQ(func2(), 7ull); func1.swap(func2); EXPECT_EQ(record1, Record(2, 0, 2)); // move, copy, dtor // An additional move and destroy into the temporary object EXPECT_EQ(record2, Record(3, 0, 3)); // move, copy, dtor EXPECT_EQ(func1(), 7ull); EXPECT_EQ(func2(), 5ull); } TEST(InPlaceFunctionTests, Conversion) { Record record; Noisy noisy{record, 15}; { InPlaceFunction<size_t(), 16> func2 = noisy; EXPECT_EQ(record, Record(0, 1, 0)); // move, copy, dtor { InPlaceFunction<size_t(), 32> func{func2}; EXPECT_EQ(record, Record(0, 2, 0)); // move, copy, dtor EXPECT_EQ(func2(), func()); } EXPECT_EQ(record, Record(0, 2, 1)); // move, copy, dtor } EXPECT_EQ(record, Record(0, 2, 2)); // move, copy, dtor } TEST(InPlaceFunctionTests, ConversionMove) { Record record; Noisy noisy{record, 15}; { InPlaceFunction<size_t(), 16> func2 = noisy; EXPECT_EQ(record, Record(0, 1, 0)); // move, copy, dtor { InPlaceFunction<size_t(), 32> func{std::move(func2)}; EXPECT_EQ(record, Record(1, 1, 0)); // move, copy, dtor EXPECT_EQ(func2(), func()); } EXPECT_EQ(record, Record(1, 1, 1)); // move, copy, dtor } EXPECT_EQ(record, Record(1, 1, 2)); // move, copy, dtor } TEST(InPlaceFunctionTests, ConversionAssign) { Record record; Noisy noisy{record, 15}; { InPlaceFunction<size_t(), 32> func; { InPlaceFunction<size_t(), 16> func2 = noisy; EXPECT_EQ(record, Record(0, 1, 0)); // move, copy, dtor func = func2; EXPECT_EQ(record, Record(0, 2, 0)); // move, copy, dtor EXPECT_EQ(func2(), func()); } EXPECT_EQ(record, Record(0, 2, 1)); // move, copy, dtor } EXPECT_EQ(record, Record(0, 2, 2)); // move, copy, dtor } TEST(InPlaceFunctionTests, ConversionAssignMove) { Record record; Noisy noisy{record, 15}; { InPlaceFunction<size_t(), 32> func; { InPlaceFunction<size_t(), 16> func2 = noisy; EXPECT_EQ(record, Record(0, 1, 0)); // move, copy, dtor func = std::move(func2); EXPECT_EQ(record, Record(1, 1, 0)); // move, copy, dtor EXPECT_EQ(func2(), func()); } EXPECT_EQ(record, Record(1, 1, 1)); // move, copy, dtor } EXPECT_EQ(record, Record(1, 1, 2)); // move, copy, dtor } template <class T, class U, class = void> struct can_assign : std::false_type {}; template <class T, class U> struct can_assign<T, U, typename std::void_t<decltype(T().operator=(U()))>> : std::true_type {}; template <class From, class To, bool Expected> static constexpr bool Convertible = (can_assign<To, From>::value == std::is_constructible_v<To, From>)&&(std::is_constructible_v<To, From> == Expected); struct TooBig { std::array<uint64_t, 5> big = {1, 2, 3, 4, 5}; size_t operator()() { return static_cast<size_t>(big[0] + big[1] + big[2] + big[3] + big[4]); } }; static_assert(sizeof(TooBig) == 40); struct NotCallable {}; struct WrongArg { void operator()(NotCallable) {} }; struct WrongRet { NotCallable operator()(size_t) { return NotCallable{}; } }; static_assert(Convertible<InPlaceFunction<size_t(), 32>, InPlaceFunction<size_t(), 32>, true>); static_assert( Convertible<InPlaceFunction<size_t(size_t), 32>, InPlaceFunction<size_t(), 32>, false>); static_assert(Convertible<InPlaceFunction<void(), 32>, InPlaceFunction<size_t(), 32>, false>); static_assert(Convertible<TooBig, InPlaceFunction<size_t(), 32>, false>); static_assert(Convertible<TooBig, InPlaceFunction<size_t(), 40>, true>); static_assert(Convertible<NotCallable, InPlaceFunction<size_t(), 40>, false>); static_assert(Convertible<WrongArg, InPlaceFunction<void(size_t), 40>, false>); static_assert(Convertible<WrongRet, InPlaceFunction<size_t(size_t), 40>, false>); // Void returning functions are modelled by any return type static_assert(Convertible<WrongRet, InPlaceFunction<void(size_t), 40>, true>); // Check constructibility/assignability from smaller function types static_assert(Convertible<InPlaceFunction<size_t(), 32>, InPlaceFunction<size_t(), 24>, false>); static_assert(Convertible<InPlaceFunction<size_t(), 32>, InPlaceFunction<size_t(), 40>, true>); static_assert( Convertible<InPlaceFunction<size_t(), 32>, InPlaceFunction<size_t(size_t), 40>, false>); static_assert( Convertible<InPlaceFunction<size_t(), 32>, InPlaceFunction<NotCallable(), 40>, false>); Loading
media/utils/include/mediautils/InPlaceFunction.h 0 → 100644 +267 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * 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 <cstdlib> #include <functional> #include <memory> #include <type_traits> namespace android::mediautils { namespace detail { // Vtable interface for erased types template <typename Ret, typename... Args> struct ICallableTable { // Destroy the erased type void (*destroy)(void* storage) = nullptr; // Call the erased object Ret (*invoke)(void* storage, Args...) = nullptr; // **Note** the next two functions only copy object data, not the vptr // Copy the erased object to a new InPlaceFunction buffer void (*copy_to)(const void* storage, void* other) = nullptr; // Move the erased object to a new InPlaceFunction buffer void (*move_to)(void* storage, void* other) = nullptr; }; } // namespace detail // This class is an *almost* drop-in replacement for std::function which is guaranteed to never // allocate, and always holds the type erased functional object in an in-line small buffer of // templated size. If the object is too large to hold, the type will fail to instantiate. // // Two notable differences are: // - operator() is not const (unlike std::function where the call operator is // const even if the erased type is not const callable). This retains const // correctness by default. A workaround is keeping InPlaceFunction mutable. // - Moving from an InPlaceFunction leaves the object in a valid state (operator // bool remains true), similar to std::optional/std::variant. // Calls to the object are still defined (and are equivalent // to calling the underlying type after it has been moved from). To opt-out // (and/or ensure safety), clearing the object is recommended: // func1 = std::move(func2); // func2 still valid (and moved-from) after this line // func2 = nullptr; // calling func2 will now abort template <typename, size_t BufferSize = 32> class InPlaceFunction; // We partially specialize to match types which are spelled like functions template <typename Ret, typename... Args, size_t BufferSize> class InPlaceFunction<Ret(Args...), BufferSize> { public: // Storage Type Details static constexpr size_t Size = BufferSize; static constexpr size_t Alignment = alignof(std::max_align_t); using Buffer_t = std::aligned_storage_t<Size, Alignment>; template <typename T, size_t Other> friend class InPlaceFunction; private: // Callable which is used for empty InPlaceFunction objects (to match the // std::function interface). struct BadCallable { [[noreturn]] Ret operator()(Args...) { std::abort(); } }; static_assert(std::is_trivially_destructible_v<BadCallable>); // Implementation of vtable interface for erased types. // Contains only static vtable instantiated once for each erased type and // static helpers. template <typename T> struct TableImpl { // T should be a decayed type static_assert(std::is_same_v<T, std::decay_t<T>>); // Helper functions to get an unerased reference to the type held in the // buffer. std::launder is required to avoid strict aliasing rules. // The cast is always defined, as a precondition for these calls is that // (exactly) a T was placement new constructed into the buffer. constexpr static T& getRef(void* storage) { return *std::launder(reinterpret_cast<T*>(storage)); } constexpr static const T& getRef(const void* storage) { return *std::launder(reinterpret_cast<const T*>(storage)); } // Constexpr implies inline constexpr static detail::ICallableTable<Ret, Args...> table = { // Stateless lambdas are convertible to function ptrs .destroy = [](void* storage) { getRef(storage).~T(); }, .invoke = [](void* storage, Args... args) -> Ret { return std::invoke(getRef(storage), args...); }, .copy_to = [](const void* storage, void* other) { ::new (other) T(getRef(storage)); }, .move_to = [](void* storage, void* other) { ::new (other) T(std::move(getRef(storage))); }, }; }; // Check size/align requirements for the T in Buffer_t. We use a templated // struct to enable std::conjunction (see below). template <typename T> struct WillFit : std::integral_constant<bool, sizeof(T) <= Size && alignof(T) <= Alignment> {}; // Check size/align requirements for a function to function conversion template <typename T> struct ConversionWillFit : std::integral_constant<bool, (T::Size < Size) && (T::Alignment <= Alignment)> {}; template <typename T> struct IsInPlaceFunction : std::false_type {}; template <size_t BufferSize_> struct IsInPlaceFunction<InPlaceFunction<Ret(Args...), BufferSize_>> : std::true_type {}; // Pred is true iff T is a valid type to construct an InPlaceFunction with // We use std::conjunction for readability and short-circuit behavior // (checks are ordered). // The actual target type is the decay of T. template <typename T> static constexpr bool Pred = std::conjunction_v< std::negation<IsInPlaceFunction<std::decay_t<T>>>, // T is not also an InPlaceFunction // of the same signature. std::is_invocable_r<Ret, std::decay_t<T>, Args...>, // correct signature callable WillFit<std::decay_t<T>> // The target type fits in local storage >; template <typename T> static constexpr bool ConvertibleFunc = std::conjunction_v<IsInPlaceFunction<std::decay_t<T>>, // implies correctly invokable ConversionWillFit<std::decay_t<T>>>; // Members below // This must come first for alignment Buffer_t storage_; const detail::ICallableTable<Ret, Args...>* vptr_; constexpr void copy_to(InPlaceFunction& other) const { vptr_->copy_to(std::addressof(storage_), std::addressof(other.storage_)); other.vptr_ = vptr_; } constexpr void move_to(InPlaceFunction& other) { vptr_->move_to(std::addressof(storage_), std::addressof(other.storage_)); other.vptr_ = vptr_; } constexpr void destroy() { vptr_->destroy(std::addressof(storage_)); } template <typename T, typename Target = std::decay_t<T>> constexpr void genericInit(T&& t) { vptr_ = &TableImpl<Target>::table; ::new (std::addressof(storage_)) Target(std::forward<T>(t)); } template <typename T, typename Target = std::decay_t<T>> constexpr void convertingInit(T&& smallerFunc) { // Redundant, but just in-case static_assert(Target::Size < Size && Target::Alignment <= Alignment); if constexpr (std::is_lvalue_reference_v<T>) { smallerFunc.vptr_->copy_to(std::addressof(smallerFunc.storage_), std::addressof(storage_)); } else { smallerFunc.vptr_->move_to(std::addressof(smallerFunc.storage_), std::addressof(storage_)); } vptr_ = smallerFunc.vptr_; } public: // Public interface template <typename T, std::enable_if_t<Pred<T>>* = nullptr> constexpr InPlaceFunction(T&& t) { genericInit(std::forward<T>(t)); } // Conversion from smaller functions. template <typename T, std::enable_if_t<ConvertibleFunc<T>>* = nullptr> constexpr InPlaceFunction(T&& t) { convertingInit(std::forward<T>(t)); } constexpr InPlaceFunction(const InPlaceFunction& other) { other.copy_to(*this); } constexpr InPlaceFunction(InPlaceFunction&& other) { other.move_to(*this); } // Making functions default constructible has pros and cons, we will align // with the standard constexpr InPlaceFunction() : InPlaceFunction(BadCallable{}) {} constexpr InPlaceFunction(std::nullptr_t) : InPlaceFunction(BadCallable{}) {} #if __cplusplus >= 202002L constexpr #endif ~InPlaceFunction() { destroy(); } // The std::function call operator is marked const, but this violates const // correctness. We deviate from the standard and do not mark the operator as // const. Collections of InPlaceFunctions should probably be mutable. constexpr Ret operator()(Args... args) { return vptr_->invoke(std::addressof(storage_), args...); } constexpr InPlaceFunction& operator=(const InPlaceFunction& other) { if (std::addressof(other) == this) return *this; destroy(); other.copy_to(*this); return *this; } constexpr InPlaceFunction& operator=(InPlaceFunction&& other) { if (std::addressof(other) == this) return *this; destroy(); other.move_to(*this); return *this; } template <typename T, std::enable_if_t<Pred<T>>* = nullptr> constexpr InPlaceFunction& operator=(T&& t) { // We can't assign to ourselves, since T is a different type destroy(); genericInit(std::forward<T>(t)); return *this; } // Explicitly defining this function saves a move/dtor template <typename T, std::enable_if_t<ConvertibleFunc<T>>* = nullptr> constexpr InPlaceFunction& operator=(T&& t) { // We can't assign to ourselves, since T is different type destroy(); convertingInit(std::forward<T>(t)); return *this; } constexpr InPlaceFunction& operator=(std::nullptr_t) { return operator=(BadCallable{}); } // Moved from InPlaceFunctions are still considered valid (similar to // std::optional). If using std::move on a function object explicitly, it is // recommended that the object is reset using nullptr. constexpr explicit operator bool() const { return vptr_ != &TableImpl<BadCallable>::table; } constexpr void swap(InPlaceFunction& other) { if (std::addressof(other) == this) return; InPlaceFunction tmp{std::move(other)}; other.destroy(); move_to(other); destroy(); tmp.move_to(*this); } friend constexpr void swap(InPlaceFunction& lhs, InPlaceFunction& rhs) { lhs.swap(rhs); } }; } // namespace android::mediautils
media/utils/tests/Android.bp +10 −0 Original line number Diff line number Diff line Loading @@ -192,3 +192,13 @@ cc_test { "extended_accumulator_tests.cpp", ], } cc_test { name: "inplace_function_tests", defaults: ["libmediautils_tests_defaults"], srcs: [ "inplace_function_tests.cpp" ], }
media/utils/tests/inplace_function_tests.cpp 0 → 100644 +346 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * 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. */ #define LOG_TAG "inplace_function_tests" #include <mediautils/InPlaceFunction.h> #include <type_traits> #include <gtest/gtest.h> #include <log/log.h> using namespace android; using namespace android::mediautils; struct BigCallable { BigCallable(size_t* x, size_t val1, size_t val2) : ptr(x), a(val1), b(val2) {} size_t* ptr; size_t a; size_t b; size_t operator()(size_t input) const { *ptr += a * 100 + b * 10 + input; return 8; } }; TEST(InPlaceFunctionTests, Basic) { size_t x = 5; InPlaceFunction<size_t(size_t)> func; { BigCallable test{&x, 2, 3}; func = test; } EXPECT_EQ(func(2), 8ull); EXPECT_EQ(x, 232ull + 5); } TEST(InPlaceFunctionTests, Invalid) { InPlaceFunction<size_t(size_t)> func; EXPECT_TRUE(!func); InPlaceFunction<size_t(size_t)> func2{nullptr}; EXPECT_TRUE(!func2); InPlaceFunction<size_t(size_t)> func3 = [](size_t x) { return x; }; EXPECT_TRUE(!(!func3)); func3 = nullptr; EXPECT_TRUE(!func3); } TEST(InPlaceFunctionTests, MultiArg) { InPlaceFunction<size_t(size_t, size_t, size_t)> func = [](size_t a, size_t b, size_t c) { return a + b + c; }; EXPECT_EQ(func(2, 3, 5), 2ull + 3 + 5); } struct Record { Record(size_t m, size_t c, size_t d) : move_called(m), copy_called(c), dtor_called(d) {} Record() {} size_t move_called = 0; size_t copy_called = 0; size_t dtor_called = 0; friend std::ostream& operator<<(std::ostream& os, const Record& record) { return os << "Record, moves: " << record.move_called << ", copies: " << record.copy_called << ", dtor: " << record.dtor_called << '\n'; } }; bool operator==(const Record& lhs, const Record& rhs) { return lhs.move_called == rhs.move_called && lhs.copy_called == rhs.copy_called && lhs.dtor_called == rhs.dtor_called; } struct Noisy { Record& ref; size_t state; Noisy(Record& record, size_t val) : ref(record), state(val) {} Noisy(const Noisy& other) : ref(other.ref), state(other.state) { ref.copy_called++; } Noisy(Noisy&& other) : ref(other.ref), state(other.state) { ref.move_called++; } ~Noisy() { ref.dtor_called++; } size_t operator()() { return state; } }; TEST(InPlaceFunctionTests, CtorForwarding) { Record record; Noisy noisy{record, 17}; InPlaceFunction<size_t()> func{noisy}; EXPECT_EQ(record, Record(0, 1, 0)); // move, copy, dtor EXPECT_EQ(func(), 17ull); Record record2; Noisy noisy2{record2, 13}; InPlaceFunction<size_t()> func2{std::move(noisy2)}; EXPECT_EQ(record2, Record(1, 0, 0)); // move, copy, dtor EXPECT_EQ(func2(), 13ull); } TEST(InPlaceFunctionTests, FunctionCtorForwarding) { { Record record; Noisy noisy{record, 17}; InPlaceFunction<size_t()> func{noisy}; EXPECT_EQ(record, Record(0, 1, 0)); // move, copy, dtor EXPECT_EQ(func(), 17ull); InPlaceFunction<size_t()> func2{func}; EXPECT_EQ(record, Record(0, 2, 0)); // move, copy, dtor EXPECT_EQ(func2(), 17ull); } Record record; Noisy noisy{record, 13}; InPlaceFunction<size_t()> func{noisy}; EXPECT_EQ(record, Record(0, 1, 0)); // move, copy, dtor EXPECT_EQ(func(), 13ull); InPlaceFunction<size_t()> func2{std::move(func)}; EXPECT_EQ(record, Record(1, 1, 0)); // move, copy, dtor EXPECT_EQ(func2(), 13ull); // We expect moved from functions to still be valid EXPECT_TRUE(!(!func)); EXPECT_EQ(static_cast<bool>(func), static_cast<bool>(func2)); EXPECT_EQ(func(), 13ull); } TEST(InPlaceFunctionTests, Dtor) { Record record; { InPlaceFunction<size_t()> func; { Noisy noisy{record, 17}; func = noisy; } EXPECT_EQ(func(), 17ull); EXPECT_EQ(record.dtor_called, 1ull); } EXPECT_EQ(record.dtor_called, 2ull); } TEST(InPlaceFunctionTests, Assignment) { { Record record; Record record2; Noisy noisy{record, 17}; Noisy noisy2{record2, 5}; InPlaceFunction<size_t()> func{noisy}; EXPECT_EQ(func(), 17ull); EXPECT_EQ(record.dtor_called, 0ull); func = noisy2; EXPECT_EQ(record.dtor_called, 1ull); EXPECT_EQ(record2, Record(0, 1, 0)); // move, copy, dtor EXPECT_EQ(func(), 5ull); } { Record record; Record record2; Noisy noisy{record, 17}; Noisy noisy2{record2, 5}; InPlaceFunction<size_t()> func{noisy}; EXPECT_EQ(func(), 17ull); EXPECT_EQ(record.dtor_called, 0ull); func = std::move(noisy2); EXPECT_EQ(record.dtor_called, 1ull); EXPECT_EQ(record2, Record(1, 0, 0)); // move, copy, dtor EXPECT_EQ(func(), 5ull); } { Record record; Record record2; Noisy noisy{record, 17}; Noisy noisy2{record2, 13}; { InPlaceFunction<size_t()> func{noisy}; EXPECT_EQ(func(), 17ull); InPlaceFunction<size_t()> func2{noisy2}; EXPECT_EQ(record2, Record(0, 1, 0)); // move, copy, dtor EXPECT_EQ(record.dtor_called, 0ull); func = func2; EXPECT_EQ(record.dtor_called, 1ull); EXPECT_EQ(func(), 13ull); EXPECT_EQ(record2, Record(0, 2, 0)); // move, copy, dtor EXPECT_TRUE(static_cast<bool>(func2)); EXPECT_EQ(func2(), 13ull); } EXPECT_EQ(record2, Record(0, 2, 2)); // move, copy, dtor } { Record record; Record record2; Noisy noisy{record, 17}; Noisy noisy2{record2, 13}; { InPlaceFunction<size_t()> func{noisy}; EXPECT_EQ(func(), 17ull); InPlaceFunction<size_t()> func2{noisy2}; EXPECT_EQ(record.dtor_called, 0ull); EXPECT_EQ(record2, Record(0, 1, 0)); // move, copy, dtor func = std::move(func2); EXPECT_EQ(record.dtor_called, 1ull); EXPECT_EQ(func(), 13ull); EXPECT_EQ(record2, Record(1, 1, 0)); // move, copy, dtor // Moved from function is still valid EXPECT_TRUE(static_cast<bool>(func2)); EXPECT_EQ(func2(), 13ull); } EXPECT_EQ(record2, Record(1, 1, 2)); // move, copy, dtor } } TEST(InPlaceFunctionTests, Swap) { Record record1; Record record2; InPlaceFunction<size_t()> func1 = Noisy{record1, 5}; InPlaceFunction<size_t()> func2 = Noisy{record2, 7}; EXPECT_EQ(record1, Record(1, 0, 1)); // move, copy, dtor EXPECT_EQ(record2, Record(1, 0, 1)); // move, copy, dtor EXPECT_EQ(func1(), 5ull); EXPECT_EQ(func2(), 7ull); func1.swap(func2); EXPECT_EQ(record1, Record(2, 0, 2)); // move, copy, dtor // An additional move and destroy into the temporary object EXPECT_EQ(record2, Record(3, 0, 3)); // move, copy, dtor EXPECT_EQ(func1(), 7ull); EXPECT_EQ(func2(), 5ull); } TEST(InPlaceFunctionTests, Conversion) { Record record; Noisy noisy{record, 15}; { InPlaceFunction<size_t(), 16> func2 = noisy; EXPECT_EQ(record, Record(0, 1, 0)); // move, copy, dtor { InPlaceFunction<size_t(), 32> func{func2}; EXPECT_EQ(record, Record(0, 2, 0)); // move, copy, dtor EXPECT_EQ(func2(), func()); } EXPECT_EQ(record, Record(0, 2, 1)); // move, copy, dtor } EXPECT_EQ(record, Record(0, 2, 2)); // move, copy, dtor } TEST(InPlaceFunctionTests, ConversionMove) { Record record; Noisy noisy{record, 15}; { InPlaceFunction<size_t(), 16> func2 = noisy; EXPECT_EQ(record, Record(0, 1, 0)); // move, copy, dtor { InPlaceFunction<size_t(), 32> func{std::move(func2)}; EXPECT_EQ(record, Record(1, 1, 0)); // move, copy, dtor EXPECT_EQ(func2(), func()); } EXPECT_EQ(record, Record(1, 1, 1)); // move, copy, dtor } EXPECT_EQ(record, Record(1, 1, 2)); // move, copy, dtor } TEST(InPlaceFunctionTests, ConversionAssign) { Record record; Noisy noisy{record, 15}; { InPlaceFunction<size_t(), 32> func; { InPlaceFunction<size_t(), 16> func2 = noisy; EXPECT_EQ(record, Record(0, 1, 0)); // move, copy, dtor func = func2; EXPECT_EQ(record, Record(0, 2, 0)); // move, copy, dtor EXPECT_EQ(func2(), func()); } EXPECT_EQ(record, Record(0, 2, 1)); // move, copy, dtor } EXPECT_EQ(record, Record(0, 2, 2)); // move, copy, dtor } TEST(InPlaceFunctionTests, ConversionAssignMove) { Record record; Noisy noisy{record, 15}; { InPlaceFunction<size_t(), 32> func; { InPlaceFunction<size_t(), 16> func2 = noisy; EXPECT_EQ(record, Record(0, 1, 0)); // move, copy, dtor func = std::move(func2); EXPECT_EQ(record, Record(1, 1, 0)); // move, copy, dtor EXPECT_EQ(func2(), func()); } EXPECT_EQ(record, Record(1, 1, 1)); // move, copy, dtor } EXPECT_EQ(record, Record(1, 1, 2)); // move, copy, dtor } template <class T, class U, class = void> struct can_assign : std::false_type {}; template <class T, class U> struct can_assign<T, U, typename std::void_t<decltype(T().operator=(U()))>> : std::true_type {}; template <class From, class To, bool Expected> static constexpr bool Convertible = (can_assign<To, From>::value == std::is_constructible_v<To, From>)&&(std::is_constructible_v<To, From> == Expected); struct TooBig { std::array<uint64_t, 5> big = {1, 2, 3, 4, 5}; size_t operator()() { return static_cast<size_t>(big[0] + big[1] + big[2] + big[3] + big[4]); } }; static_assert(sizeof(TooBig) == 40); struct NotCallable {}; struct WrongArg { void operator()(NotCallable) {} }; struct WrongRet { NotCallable operator()(size_t) { return NotCallable{}; } }; static_assert(Convertible<InPlaceFunction<size_t(), 32>, InPlaceFunction<size_t(), 32>, true>); static_assert( Convertible<InPlaceFunction<size_t(size_t), 32>, InPlaceFunction<size_t(), 32>, false>); static_assert(Convertible<InPlaceFunction<void(), 32>, InPlaceFunction<size_t(), 32>, false>); static_assert(Convertible<TooBig, InPlaceFunction<size_t(), 32>, false>); static_assert(Convertible<TooBig, InPlaceFunction<size_t(), 40>, true>); static_assert(Convertible<NotCallable, InPlaceFunction<size_t(), 40>, false>); static_assert(Convertible<WrongArg, InPlaceFunction<void(size_t), 40>, false>); static_assert(Convertible<WrongRet, InPlaceFunction<size_t(size_t), 40>, false>); // Void returning functions are modelled by any return type static_assert(Convertible<WrongRet, InPlaceFunction<void(size_t), 40>, true>); // Check constructibility/assignability from smaller function types static_assert(Convertible<InPlaceFunction<size_t(), 32>, InPlaceFunction<size_t(), 24>, false>); static_assert(Convertible<InPlaceFunction<size_t(), 32>, InPlaceFunction<size_t(), 40>, true>); static_assert( Convertible<InPlaceFunction<size_t(), 32>, InPlaceFunction<size_t(size_t), 40>, false>); static_assert( Convertible<InPlaceFunction<size_t(), 32>, InPlaceFunction<NotCallable(), 40>, false>);