xref: /aosp_15_r20/frameworks/av/media/utils/include/mediautils/InPlaceFunction.h (revision ec779b8e0859a360c3d303172224686826e6e0e1)
1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 #pragma once
18 
19 #include <cstdlib>
20 #include <functional>
21 #include <memory>
22 #include <type_traits>
23 
24 namespace android::mediautils {
25 
26 namespace detail {
27 // Vtable interface for erased types
28 template <typename Ret, typename... Args>
29 struct ICallableTable {
30     // Destroy the erased type
31     void (*destroy)(void* storage) = nullptr;
32     // Call the erased object
33     Ret (*invoke)(void* storage, Args&&...) = nullptr;
34     // **Note** the next two functions only copy object data, not the vptr
35     // Copy the erased object to a new InPlaceFunction buffer
36     void (*copy_to)(const void* storage, void* other) = nullptr;
37     // Move the erased object to a new InPlaceFunction buffer
38     void (*move_to)(void* storage, void* other) = nullptr;
39 };
40 }  // namespace detail
41 
42 // This class is an *almost* drop-in replacement for std::function which is guaranteed to never
43 // allocate, and always holds the type erased functional object in an in-line small buffer of
44 // templated size. If the object is too large to hold, the type will fail to instantiate.
45 //
46 // Some notable differences are:
47 // - operator() is not const (unlike std::function where the call operator is
48 // const even if the erased type is not const callable). This retains const
49 // correctness by default. A workaround is keeping InPlaceFunction mutable.
50 // - Moving from an InPlaceFunction leaves the object in a valid state (operator
51 // bool remains true), similar to std::optional/std::variant.
52 // Calls to the object are still defined (and are equivalent
53 // to calling the underlying type after it has been moved from). To opt-out
54 // (and/or ensure safety), clearing the object is recommended:
55 //      func1 = std::move(func2); // func2 still valid (and moved-from) after this line
56 //      func2 = nullptr; // calling func2 will now abort
57 // - Unsafe implicit conversions of the return value to a reference type are
58 // prohibited due to the risk of dangling references (some of this safety was
59 // added to std::function in c++23). Only converting a reference to a reference to base class is
60 // permitted:
61 //      std::function<Base&()> = []() -> Derived& {...}
62 // - Some (current libc++ implementation) implementations of std::function
63 // incorrectly fail to handle returning non-moveable types which is valid given
64 // mandatory copy elision.
65 //
66 // Additionally, the stored functional will use the typical rules of overload
67 // resolution to disambiguate the correct call, except, the target class will
68 // always be implicitly a non-const lvalue when called. If a different overload
69 // is preferred, wrapping the target class in a lambda with explicit casts is
70 // recommended (or using inheritance, mixins or CRTP). This avoids the
71 // complexity of utilizing abonimable function types as template params.
72 template <typename, size_t BufferSize = 32>
73 class InPlaceFunction;
74 // We partially specialize to match types which are spelled like functions
75 template <typename Ret, typename... Args, size_t BufferSize>
76 class InPlaceFunction<Ret(Args...), BufferSize> {
77   public:
78     // Storage Type Details
79     static constexpr size_t Size = BufferSize;
80     static constexpr size_t Alignment = alignof(std::max_align_t);
81     using Buffer_t = std::aligned_storage_t<Size, Alignment>;
82     template <typename T, size_t Other>
83     friend class InPlaceFunction;
84 
85   private:
86     // Callable which is used for empty InPlaceFunction objects (to match the
87     // std::function interface).
88     struct BadCallable {
operatorBadCallable89         [[noreturn]] Ret operator()(Args...) { std::abort(); }
90     };
91     static_assert(std::is_trivially_destructible_v<BadCallable>);
92 
93     // Implementation of vtable interface for erased types.
94     // Contains only static vtable instantiated once for each erased type and
95     // static helpers.
96     template <typename T>
97     struct TableImpl {
98         // T should be a decayed type
99         static_assert(std::is_same_v<T, std::decay_t<T>>);
100 
101         // Helper functions to get an unerased reference to the type held in the
102         // buffer. std::launder is required to avoid strict aliasing rules.
103         // The cast is always defined, as a precondition for these calls is that
104         // (exactly) a T was placement new constructed into the buffer.
getRefTableImpl105         constexpr static T& getRef(void* storage) {
106             return *std::launder(reinterpret_cast<T*>(storage));
107         }
108 
getRefTableImpl109         constexpr static const T& getRef(const void* storage) {
110             return *std::launder(reinterpret_cast<const T*>(storage));
111         }
112 
113         // Constexpr implies inline
114         constexpr static detail::ICallableTable<Ret, Args...> table = {
115                 // Stateless lambdas are convertible to function ptrs
116                 .destroy = [](void* storage) { getRef(storage).~T(); },
117                 .invoke = [](void* storage, Args&&... args) -> Ret {
118                     if constexpr (std::is_void_v<Ret>) {
119                         std::invoke(getRef(storage), std::forward<Args>(args)...);
120                     } else {
121                         return std::invoke(getRef(storage), std::forward<Args>(args)...);
122                     }
123                 },
124                 .copy_to = [](const void* storage,
125                               void* other) { ::new (other) T(getRef(storage)); },
126                 .move_to = [](void* storage,
127                               void* other) { ::new (other) T(std::move(getRef(storage))); },
128         };
129     };
130 
131     // Check size/align requirements for the T in Buffer_t.
132     template <typename T>
133     static constexpr bool WillFit_v = sizeof(T) <= Size && alignof(T) <= Alignment;
134 
135     // Check size/align requirements for a function to function conversion
136     template <typename T>
137     static constexpr bool ConversionWillFit_v = (T::Size < Size) && (T::Alignment <= Alignment);
138 
139     template <typename T>
140     struct IsInPlaceFunction : std::false_type {};
141 
142     template <size_t BufferSize_>
143     struct IsInPlaceFunction<InPlaceFunction<Ret(Args...), BufferSize_>> : std::true_type {};
144 
145     template <typename T>
146     static T BetterDeclval();
147     template <typename T>
148     static void CheckImplicitConversion(T);
149 
150     template <class T, class U, class = void>
151     struct CanImplicitConvert : std::false_type {};
152 
153     // std::is_convertible/std::invokeable has a bug (in libc++) regarding
154     // mandatory copy elision for non-moveable types. So, we roll our own.
155     // https://github.com/llvm/llvm-project/issues/55346
156     template <class From, class To>
157     struct CanImplicitConvert<From, To,
158                               decltype(CheckImplicitConversion<To>(BetterDeclval<From>()))>
159         : std::true_type {};
160 
161     // Check if the provided type is a valid functional to be type-erased.
162     // if constexpr utilized for short-circuit behavior
163     template <typename T>
164     static constexpr bool isValidFunctional() {
165         using Target = std::decay_t<T>;
166         if constexpr (IsInPlaceFunction<Target>::value || std::is_same_v<Target, std::nullptr_t>) {
167             // Other overloads handle these cases
168             return false;
169         } else if constexpr (std::is_invocable_v<Target, Args...>) {
170             // The target type is a callable (with some unknown return value)
171             if constexpr (std::is_void_v<Ret>) {
172                 // Any return value can be dropped to model a void returning
173                 // function.
174                 return WillFit_v<Target>;
175             } else {
176                 using RawRet = std::invoke_result_t<Target, Args...>;
177                 if constexpr (CanImplicitConvert<RawRet, Ret>::value) {
178                     if constexpr (std::is_reference_v<Ret>) {
179                         // If the return type is a reference, in order to
180                         // avoid dangling references, we only permit functionals
181                         // which return a reference to the exact type, or a base
182                         // type.
183                         if constexpr (std::is_reference_v<RawRet> &&
184                                       (std::is_same_v<std::decay_t<Ret>, std::decay_t<RawRet>> ||
185                                        std::is_base_of_v<std::decay_t<Ret>,
186                                                          std::decay_t<RawRet>>)) {
187                             return WillFit_v<Target>;
188                         }
189                         return false;
190                     }
191                     return WillFit_v<Target>;
192                 }
193                 // If we can't convert the raw return type, the functional is invalid.
194                 return false;
195             }
196         }
197         return false;
198     }
199 
200     template <typename T>
201     static constexpr bool IsValidFunctional_v = isValidFunctional<T>();
202     // Check if the type is a strictly smaller sized InPlaceFunction
203     template <typename T>
204     static constexpr bool isConvertibleFunc() {
205         using Target = std::decay_t<T>;
206         if constexpr (IsInPlaceFunction<Target>::value) {
207             return ConversionWillFit_v<Target>;
208         }
209         return false;
210     }
211 
212     template <typename T>
213     static constexpr bool IsConvertibleFunc_v = isConvertibleFunc<T>();
214 
215     // Members below
216     // This must come first for alignment
217     Buffer_t storage_;
218     const detail::ICallableTable<Ret, Args...>* vptr_;
219 
220     constexpr void copy_to(InPlaceFunction& other) const {
221         vptr_->copy_to(std::addressof(storage_), std::addressof(other.storage_));
222         other.vptr_ = vptr_;
223     }
224 
225     constexpr void move_to(InPlaceFunction& other) {
226         vptr_->move_to(std::addressof(storage_), std::addressof(other.storage_));
227         other.vptr_ = vptr_;
228     }
229 
230     constexpr void destroy() { vptr_->destroy(std::addressof(storage_)); }
231 
232     template <typename T, typename Target = std::decay_t<T>>
233     constexpr void genericInit(T&& t) {
234         vptr_ = &TableImpl<Target>::table;
235         ::new (std::addressof(storage_)) Target(std::forward<T>(t));
236     }
237 
238     template <typename T, typename Target = std::decay_t<T>>
239     constexpr void convertingInit(T&& smallerFunc) {
240         // Redundant, but just in-case
241         static_assert(Target::Size < Size && Target::Alignment <= Alignment);
242         if constexpr (std::is_lvalue_reference_v<T>) {
243             smallerFunc.vptr_->copy_to(std::addressof(smallerFunc.storage_),
244                                        std::addressof(storage_));
245         } else {
246             smallerFunc.vptr_->move_to(std::addressof(smallerFunc.storage_),
247                                        std::addressof(storage_));
248         }
249         vptr_ = smallerFunc.vptr_;
250     }
251 
252   public:
253     // Public interface
254     template <typename T, std::enable_if_t<IsValidFunctional_v<T>>* = nullptr>
255     constexpr InPlaceFunction(T&& t) {
256         genericInit(std::forward<T>(t));
257     }
258 
259     // Conversion from smaller functions.
260     template <typename T, std::enable_if_t<IsConvertibleFunc_v<T>>* = nullptr>
261     constexpr InPlaceFunction(T&& t) {
262         convertingInit(std::forward<T>(t));
263     }
264 
265     constexpr InPlaceFunction(const InPlaceFunction& other) { other.copy_to(*this); }
266 
267     constexpr InPlaceFunction(InPlaceFunction&& other) { other.move_to(*this); }
268 
269     // Making functions default constructible has pros and cons, we will align
270     // with the standard
271     constexpr InPlaceFunction() : InPlaceFunction(BadCallable{}) {}
272 
273     constexpr InPlaceFunction(std::nullptr_t) : InPlaceFunction(BadCallable{}) {}
274 
275 #if __cplusplus >= 202002L
276     constexpr ~InPlaceFunction() {
277 #else
278     ~InPlaceFunction() {
279 #endif
280         destroy();
281     }
282 
283     // The std::function call operator is marked const, but this violates const
284     // correctness. We deviate from the standard and do not mark the operator as
285     // const. Collections of InPlaceFunctions should probably be mutable.
286     constexpr Ret operator()(Args... args) {
287         if constexpr (std::is_void_v<Ret>) {
288             vptr_->invoke(std::addressof(storage_), std::forward<Args>(args)...);
289         } else {
290             return vptr_->invoke(std::addressof(storage_), std::forward<Args>(args)...);
291         }
292     }
293 
294     constexpr InPlaceFunction& operator=(const InPlaceFunction& other) {
295         if (std::addressof(other) == this) return *this;
296         destroy();
297         other.copy_to(*this);
298         return *this;
299     }
300 
301     constexpr InPlaceFunction& operator=(InPlaceFunction&& other) {
302         if (std::addressof(other) == this) return *this;
303         destroy();
304         other.move_to(*this);
305         return *this;
306     }
307 
308     template <typename T, std::enable_if_t<IsValidFunctional_v<T>>* = nullptr>
309     constexpr InPlaceFunction& operator=(T&& t) {
310         // We can't assign to ourselves, since T is a different type
311         destroy();
312         genericInit(std::forward<T>(t));
313         return *this;
314     }
315 
316     // Explicitly defining this function saves a move/dtor
317     template <typename T, std::enable_if_t<IsConvertibleFunc_v<T>>* = nullptr>
318     constexpr InPlaceFunction& operator=(T&& t) {
319         // We can't assign to ourselves, since T is different type
320         destroy();
321         convertingInit(std::forward<T>(t));
322         return *this;
323     }
324 
325     constexpr InPlaceFunction& operator=(std::nullptr_t) { return operator=(BadCallable{}); }
326 
327     // Moved from InPlaceFunctions are still considered valid (similar to
328     // std::optional). If using std::move on a function object explicitly, it is
329     // recommended that the object is reset using nullptr.
330     constexpr explicit operator bool() const { return vptr_ != &TableImpl<BadCallable>::table; }
331 
332     constexpr void swap(InPlaceFunction& other) {
333         if (std::addressof(other) == this) return;
334         InPlaceFunction tmp{std::move(other)};
335         other.destroy();
336         move_to(other);
337         destroy();
338         tmp.move_to(*this);
339     }
340 
341     friend constexpr void swap(InPlaceFunction& lhs, InPlaceFunction& rhs) { lhs.swap(rhs); }
342 };
343 
344 }  // namespace android::mediautils
345