Named Arguments
In order to support automated exporting to Python, BMM needs a way to encode
the function argument list of a constructor. This idea further expanded to
also relax the rigid way arguments are passed in C++.
Named arguments consist of two parts, the specification of individual function arguments (bbm::arg), and a structure for specifying a sequence of arguments (bbm::args).
bbm::arg
bbm::arg is a structure to store the relevant information (compile and
run-time) that mimics the behavior of an argument. This information is:
argument type
argument name (stored as a
bbm::string_literal)the default value (passed as a stateless lambda function)
the value of the argument; including support for rvalues.
This is achieved via template specialization. The definition of bbm::arg:
-
template<typename Type, string_literal Name, typename Default = void>
struct arg Forward declaration of bbm::arg.
- Template Parameters:
Type – = type of the argument
Name – = argument name
Default – = invocable type that returns the default value
A typedef bbm::is_arg_v is included to detect if a type is a bbm::arg.
The five specializations are:
an untyped arguments:
bbm::arg<void, Name, void>. One can assign a value to this argument. The resulting type of the assignment, however, is one of the four other specializations. I.e., the result of the assignment is a different type. The custom string literal""_argcreates this specialization:auto a = "test"_arg; std::cout << toTypestring(a) << std::endl; // bbm::arg<void, bbm::string_literal<5>{"test"}, void> std::cout << a << std::endl; // test auto b = (a = 3); // changes type on assignment std::cout << toTypestring(b) << std::endl; // bbm::arg<const int&, bbm::string_literal<5>{"test"}, void> std::cout << b << std::endl; // const int& test = 3
A
bbm::argcontains a typedef alias to itstype, and astatic constexpr string_literal name;a non-reference type without default value:
bbm::arg<Type, Name, void>with requirement:(!std::is_void_v<Type> && !std::is_reference_v<Type>)). This corresponds to the type ofbin the previous example. Thisbbm::argvariant has a private class attribute:_valueto store the argument value. In addition to the assignment operator and constructors, this specialization also contains cast operators to the underlying value type. Note: the assignment operator will assign the value tobbm::argand*thisis returned.a non-reference type with default value:
bbm::arg<Type, Name, Default>with the requirement that:(!std::is_void_v<Type> && !std::is_reference_v<Type>) && std::invocable<Default>. The latter constraint implies thatDefaultis a lambda function or a functor. The difference withbbm::argwithout default value is that the trivial constructor sets the value to the default value.To facilitate passing a default value, a helper macro
ArgDef(...)is defined that creates a stateless lambda that return the macro argument.bbm::arg<float, "test", ArgDef(4.0)> a; std::cerr << a << std::endl; // float test = 4.000000 [ = 4.000000] a = 3.0; std::cerr << a << std::endl; // float test = 3.000000 [ = 4.0000000]
a reference type without default value:
bbm::arg<Type, Name, void>with the requirement:(!std::is_void_v<Type> && std::is_reference_v<Type>). The private class attribute is declared as:bbm::persistent_reference<Type> _value. A key difference with the previous two specialization is that the assignment operator re-assigns the reference, not the value! A custom cast operator toconst type&andtype&is included.float f = 3.0; auto a = ("test"_arg = f); std::cout << toTypestring(a) << std::endl; // bbm::arg<float&, bbm::string_literal<5>{"test"}, void> std::cout << a << std::endl; // float& test = 3.000000 f = 4.0; std::cout << a << std::endl; // float& test = 4.000000
The first half, we can see that assigning a variable ‘f’ creates as expected a
bbm::argwith a reference tof. In the second half we can see that changingfis reflected ina(because it is a reference tof).a reference type with default value: exactly the same as the previous with exception of the trivial constructor. The underlying
bbm::persistent_referencetakes care of keeping rvalues in scope.bbm::arg<const float&, "test", ArgDef(4.0)> a; std::cout << a << std::endl; // const float& test = 4.000000 [ = 4.000000] bbm::arg<const float&, "test2", ArgDef(4.0)> b = 3.0; std::cout << b << std::endl; // const float& test2 = 3.000000 [ = 4.000000] float f = 2.0; bbm::arg<const float&, "test3", ArgDef(4.0)> c = f; std::cout << c << std::endl; // const float& test3 = 2.000000 [ = 4.000000]
testwill reference a temporary float that contains the value4.0(set from the default value).test2also creates a temporary float in which the value3.0is copied.test3contains a reference tofwithout allocating any new temporary memory.
bbm::args
bbm::args is a collection of bbm:arg:
-
template<typename ...ARGS>
struct args
Usage Examples
using namespace bbm;
void foo( args<arg<float, "a">, arg<float, "b">> myargs ) {}
// either pass with {} or by explicit cast
foo( {1, 2} );
foo( args<arg<float, "a">, arg<float, "b">>(1, 2) );
// can use names or position to pass
foo( {"a"_arg = 1, "b"_arg = 2} );
foo( {"b"_arg = 2, "a"_arg = 1} );
foo( { 1, "b"_arg = 2} );
// pass by explicit denote position:
foo( {"0"_arg = 1, "1"_arg = 2} );
foo( {"1"_arg = 1, "0"_arg = 2} );
The above example shows different ways to pass arguments.
One can embed the arguments in curly brackets (i.e., an initializer list). If the compiler is not able to figure out the type, explicit casting is required (2nd example).
We can also exploit the custom literal
""_argto pass arguments. Note that the constructor ofbbm::argswill matchbbm::argby name when constructingbbm::args. If no name is provided,bbm::argsuse the position in the list to assign it to the correctbbm::arg.The
bbm::argsconstructor will interpret numerical argument names as positions in the argument list. Hence,"0"_arg = 1assign ‘’1’’ to the zero-thbbm::arg.Another assignment mechanism (not shown) is when the type of the argument is unique. In that case, all of the above solution for matching fail, the argument will be assigned based on compatible (unique) type.
To access the values stored in a bbm::args:
std::cout << myargs.template value<0>() << std::endl; // zero-th argument
std::cout << myargs.template value<"a">() << std::endl; // argument named "a"
std::cout << myargs.value("a"_arg) << std::endl; // argument named "a"
Alternatively, a helper macro can create aliases:
BBM_IMPORT_ARGS(myargs, a, b);
std::cerr << a << std::endl; // alias 'a' is create to args.value("a"_arg)
bbm::args contains a number of other useful typedefs and methods:
using T = decltype(myargs);
myargs.size; // number of arguments
T::size; // number of arguments
myargs.values() // type with all (references to) the argument values
myargs.get("a"_arg); // return bbm:arg matching "a"
myarg.value("a"_arg); // return bbm::arg::type matching "a"
T::is_compatible<int, std::string>; // true if passing (int, std::string) can be used to construct T
T::is_cpp_compatible<int, float>; // true if passing (int, float) is compatible when using classic C++ passing mechanism.
Having the encapsulate the arguments in curly brackets or by explicit casting
is a but cumbersome. Therefore, BBM has four different macros to help create
a specialized function that takes a parameter pack as input, and forwards it
to a method with a bbm::args argument. The is_compatible constexpr
boolean is used to constrain the forwarding method to only compatible packs:
BBM_FORWARD_ARGS(foo, arg<float, "a">, arg<float, "b">);
foo(1, 2); // forwarded
foo(1, "b"_arg = 2); // forwarded
foo("a"_arg = 1, "b"_arg = 2); // forwarded
BBM_FORWARD_ARGS_CONST defined the forwarding method as a const
method.
The above macro still expects that the method that is being forwarded to has a
bbm::args argument. However, it is also possible to pass to a regular
function (i.e., basically extend the flexibility in how the method can be
called):
void bar( float a, int b=1 ) {}
BBM_FORWARD_CPP_ARGS(bar, arg<float, "a">, arg<int, "b", ArgDef(1)>);
bar(1); // direct cpp call; no forwarding
bar(1, 2); // direct cpp call; no forwarding
bar("b"_arg = 2, "a"_arg = 1); // forwarding
Similar as before BBM_FORWARD_CPP_ARGS_CONST defines the forwarding method
as const.
Warning
The bbm:arg list passed to BBM_FORWARD_CPP_ARGS must match exactly
the definition of the target method, including default arguments. This
is necessary to determine whether or not the method can be called directly
(is_cpp_compatible).
Implementation
Please refer to include/core/arg.h
and include/core/args.h for
the full implementation details. In what follows, we will focus on how BBM
resolves which constructor argument to assign to which bbm::arg.
The whole process of matching arguments is performed in constexpr during
compile time. Hence, there is no run-time overhead. When passing a series of
arguments to the constructor of bbm::args, the constructor forwards them
as a tuple to a private method _retrieve_args which creates each
bbm::arg in order as defined in bbm::args``(using the private method
``_retrieve_arg) by searching through the forwarded tuple of constructor
arguments for a matching constructor argument. Hence, the (compile) time
complexity is roughly squared with respect to the number of arguments. If no
such element is found in the forwarded constructor arguments, then the default
value of the target bbm::arg is tried. If no default argument exists, a
compile error is generated.
The key method in finding a matching argument in the forwarded constructor
arguments is the _find_arg_index method that takes the index of the
bbm::arg we are trying to match, and the forwarded tuple type (we do not
need to values to determine which argument is the best match), and it returns
the index (in the forwarded constructor arguments) that is the best match.
_find_arg_index<IDX, TUP> proceeds in the following order:
we scan the whole TUP to see if there is an element with a matching name to the IDX-th
bbm::arg. If a match is found, and the type is compatible with the IDX-thbbm::arg, then the search process is terminated the matching index (in TUP) is returned.if there exists an argument in TUP with name “IDX” and the type is compatible, then the matching index (in TUP) is returned.
check if TUP[IDX] is compatible with the IDX-th
bbm::arg. If the TUP[IDX] element is also abbm::arg, then the name must be empty (to avoid assign"b"_arg = 1at position0to a bbm::arg` with a different name). If compatible, return IDX.if the type of the IDX-th
bbm::argis unique, and there is a matching unique type in TUP, then return the index (in TUP).else: fail (return index outside range).