![]() |
Arene Base
Fundamental Utilities For Safety Critical C++
|
Arene Base provides facilities to make it easier to write code that uses invocable types.
The public header is
The Bazel target is
There are many ways of writing expressions that are invocable: functions, lambdas, class types with operator(), function pointers, member function pointers, etc. Dealing with all of these when accepting something that should be invoked within generic code can be complex, and type-erased implementations using std::function. can allocate dynamic memory. The functional sub-package provides facilities to make dealing with such cases easier, especially in combination with the arene::base::inline_function class template, and arene::base::is_invocable from the type_traits subpackage.
arene::base::invoke provides a way to invoke any invocable with a specified set of arguments, seamlessly handling functions, lambdas and member function pointers. It is a back-port of std::invoke.
arene::base::function_ref provides a type-erased class template that references an invocable, for use as a function argument. This is a back-port of std::function_ref.
arene::base::function_traits provides a way to get traits for any given function type. This includes member function pointers.
arene::base::bind_front provides a way to bind arguments to the front of an existing invocable for argument currying without needing hand-written lambdas. This is a backport of std::bind_front
arene::base::bind_back provides a way to bind arguments to the back of an existing invocable for argument currying without needing hand-written lambdas. This is a backport of std::bind_back
arene::base::bind_overloads provides a way to easily combine function objects (generally lambdas) together into a single invocable. It's generally used for creating visitors for variants.
arene::base::not_fn provides a way to negate the return of existing invocable for functional composition without needing hand-written lambdas. This is a backport of std::not_fn
Invocables are used in multiple contexts:
The common property in all these cases is that the invocable is being used to customize the operation of the code to which it is being passed.
There are fundamentally two ways to do this:
std::function, arene::base::function_ref, or arene::base::inline_function.Which is better depends on the context.
The common pattern for class templates that need an invocable is to specify the type of the invocable with a template parameter, and then have the constructor accept a concrete instance to be used, possibly using a default-constructed instance by default.
Inside the class template implementation, the invocable will then be invoked with some specific set of arguments:
This works well, but has downsides. Firstly, if you pass an invocable that accepts the wrong set of parameters, or returns the wrong type, then the errors refer to the member function that actually invoked it, rather than the point of use of the class template. This can lead to messy error messages with multiple layers of template instantiations, hiding the source of the error.
Secondly, it limits the category of invocables to those that can be invoked directly with the function call syntax (thus ruling out member function pointers, for example).
Thirdly, it prevents the use of lambdas, since the type of each lambda is unique, even if they are token-by-token identical.
The first problem can usually be solved by adding an appropriate static_assert to the class template body, using arene::base::is_invocable_v or arene::base::is_invocable_r_v as the condition. This will trigger errors immediately upon instantiation of the class template, rather than waiting for the appropriate member function to be invoked, and provides an opportunity for a clear error message.
If we specify an incompatible comparator, then we get an error message from the static_assert:
The compiler now generates a concise error referencing the static_assert and the container instantiation:
The second problem is solved by using arene::base::invoke rather than calling the invocable directly:
This would then allow Comparator to be a member function pointer: bool MyData::*(const MyData&) const
The solution to the third problem (using lambdas) is to use a type-erased invocable such as std::function or arene::base::inline_function as the template parameter. This will allow passing any invocable that can be stored in the type-erased wrapper, at the cost of a pointer indirection for every call, and possibly dynamic memory allocation (with std::function).
Function templates that need to accept invocables, such as std::for_each, commonly have a template parameter that specifies the type of the invocable, and then a function parameter for the actual value:
This works better than the class template case, because the template parameter is usually deduced rather than being explicitly specified. This means that it works seamlessly with lambdas, but the other problems are still present.
The same solutions work for function templates too: use static_cast to get a sensible error message if the invocable can't be invoked with the desired arguments, and use arene::base::invoke to allow usage with other invocables.
Function templates also have an additional mechanism for improving the error messages: we can use constraints to remove the function from the overload set if the invocable doesn't meet the constraints.
If there is not a viable overload of the function for a given call, then the compiler gives a friendly error message relating to the constraint:
Here, the error message clearly identifies that the is_invocable_v requirement was not satisfied:
Where it is desirable for a function to accept any invocable which can be invoked with a specific set of arguments, but it is not desirable to make the function a template, arene::base::function_ref can be used as the function argument. This avoids copying the invocable, which would be required std::function or arene::base::inline_function is used. This is a back-port of std::function_ref. This allows the function to be declared in a header, and defined in a .cpp file.
arene::base::function_ref binds a reference to the supplied invocable, so should always have a lifetime that is shorter than the referenced invocable. Using it exclusively for function arguments ensures this requirement is upheld.arene::base::function_ref objects to a type-erasing invocable like std::function or arene::base::inline_function will store a copy of the arene::base::function_ref object. This will still reference the original invocable, so is not recommended.If you want to store an invocable for later use, but cannot make the type of the invocable a template parameter, then you need a type-erased invocable wrapper. The C++ Standard Library provides std::function and std::move_only_function for this purpose, but these use dynamic memory allocation, and std::move_only_function is only available from C++23. The functional subpackage fills this need with arene::base::inline_function, which is an inline container, so stores the invocable internally.
Here, my_class stores a callback internally that is invoked later as part of the do_stuff member function:
my_class::do_stuff can be defined out of line, in the .cpp file, since my_class is not a template, and thus can isolate the make_data function from the user of my_class.
It can be useful to get the traits associated with a given function type. Using arene::base::function_traits templated on a function type will give a class with the following member types:
return_type: The type returned by the functionargument_types: An arene::base::type_list of the types expected as argumentsAdditionally, these types can be accessed directly witharene::base::function_return_t and arene::base::function_arguments_t, respectively.
Traits are accessed from a member pointer via arene::base::member_pointer_traits. This includes both function and non-function member pointers. These traits are:
class_type: The class the pointee is a member ofpointee_type: The type being pointed toarene::base::member_function_pointer_traits is a combination of both above trait types for member function pointers. The equivalent return and argument types for member function pointers are arene::base::member_function_return_t and arene::base::member_function_arguments_t. Additionally, it also contains the member type implicit_argument_type which is class_type qualified to match the qualifiers for pointee_type. When invoking a member function pointer this will implicitly be the first argument.
function_traits also contains static constexpr enum members specfying the qualifiers for the underlying function. These enums are arene::base::cv_qualifier and arene::base::reference_qualifer. For function types these are both always unqualified. These are available as std::integral_constant for member function pointers as arene::base::member_function_cv and arene::base::mamber_function_reference or directly as enums as arene::base::member_function_cv_v and arene::base::member_function_reference_v.