homogeneous variadic function parameters
Copyright Tobias Loew 2019.
Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
hop is a small library that allows to create proper homogeneous variadic function parameters
proper means, that the functions you equip with hop's homogeneous variadic parameters are subject to C++ overload resolution. Let me show you an example:
Suppose you want to have a function foo
that accepts an arbitrary non-zero number of int
arguments.
The traditional solution from the pre C++11 age was to create overloads for foo
up to a required/reasonable number of arguments
void foo(int n1);
void foo(int n1, int n2);
void foo(int n1, int n2, int n3);
void foo(int n1, int n2, int n3, int n4);
Now, with C++11 and variadic Templates, we can write the whole overload-set as a single function
template<typename... Args>
void foo(Args&&... args);
but wait, we haven't said anything about int
- specified as above, foo
can be called with any list of parameters. So, how can we constrain foo
to only accept argument-list containing one or more int
arguments ? Of course, we use SFINAE
template<typename... Ts>
using AllInts = typename std::conjunction<std::is_convertible<Ts, int>...>::type;
template<typename... Ts, typename = std::enable_if_t<AllInts<Ts...>::value, void>>
void foo(Ts&& ... ts) {}
in the same way we can do this for double
template<typename... Ts>
using AllDoubles = typename std::conjunction<std::is_convertible<Ts, double>...>::type;
template<typename... Ts, typename = std::enable_if_t<AllDoubles<Ts...>::value, void>>
void foo(Ts&& ... ts) {}
But, when we use both overload set together, we get an error that foo
is defined twice. ((C++17; §17.1.16) A template-parameter shall not be given default arguments by two different declarations in the same scope.) cf. https://www.fluentcpp.com/2018/05/15/make-sfinae-pretty-1-what-value-sfinae-brings-to-code/
One possible solution is
template<typename... Ts>
using AllInts = typename std::conjunction<std::is_convertible<Ts, int>...>::type;
template<typename... Ts, typename std::enable_if_t<AllInts<Ts...>::value, int> = 0>
void foo(Ts&& ... ts) {}
template<typename... Ts>
using AllDoubles = typename std::conjunction<std::is_convertible<Ts, double>...>::type;
template<typename... Ts, typename std::enable_if_t<AllDoubles<Ts...>::value, int> = 0>
void foo(Ts&& ... ts) {}
But when we now call foo(42)
or foo(0.5, -1.3)
we always get ambigous call errors - and that's absolutely correct: both foo
templates accept the argument-lists (int
is convertible to double
and vice-versa) and both take their arguments as forwarding-references so they're both equivalent perfect matches - bang!
And here we are at the core of the problem: when we have multiple functions defined as above C++'s overload resolution won't step in to select the best match - they're all best matches (as long as we only consider only template functions). And here hop can help...
With hop we define only a single overload of foo
but with a quite sophisticated SFINAE condition:
using overloads = hop::ol_list <
hop::ol<hop::non_empty_pack<int>>,
hop::ol<hop::non_empty_pack<double>>
>;
template<typename... Ts, hop::enable_test<overloads, Ts...> = 0 >
void foo(Ts&& ... ts) {
using OL = hop::enable_t<overloads, Ts...>;
}
Now, we can call foo
the same way we did for the traditional (bounded) overload-sets:
foo(42, 17);
foo(1.5, -0.4, 12.0);
foo(42, 0.5); // error: ambigous
Let's take a look a the types that are involved:
hop::enable_t<overloads, Ts...>
is defined asdecltype(hop::enable<overloads, Ts...>())
and holds the information about the selected overload
while the almost identical
hop::enable_test<overloads, Ts...>
is defined asdecltype((hop::enable<overloads, Ts...>()), 0)
and can be used as SFINAE condition (as non-type template parameter of typeint
, which usually has the default value0
).
using overloads = hop::ol_list <
hop::ol<int, hop::non_empty_pack<int>>,
hop::ol<double, hop::non_empty_pack<double>>
>;
template<typename Out, typename T>
void output_as(T&& t) {
std::cout << (Out)t << std::endl;
}
template<typename... Ts, hop::enable_test<overloads, Ts...> = 0 >
void foo(Ts&& ... ts) {
using OL = hop::enable_t<overloads, Ts...>;
if constexpr (hop::index<OL>::value == 0) {
std::cout << "got a bunch of ints\n";
(output_as<int>(ts),...);
std::cout << std::endl;
}
else
if constexpr (hop::index<OL>::value == 1) {
std::cout << "got a bunch of doubles\n";
(output_as<double>(ts), ...);
std::cout << std::endl;
}
}
output
got a bunch of ints
42
17
got a bunch of doubles
1.5
-0.4
12
Alternatively, we can tag an overload, and test for it:
struct tag_ints {};
struct tag_doubles {};
using overloads = hop::ol_list <
hop::tagged_ol<tag_ints, hop::non_empty_pack<int>>,
hop::tagged_ol<tag_doubles, hop::non_empty_pack<double>>
>;
template<typename... Ts, hop::enable_test<overloads, Ts...> = 0 >
void foo(Ts&& ... ts) {
using OL = hop::enable_t<overloads, Ts...>;
if constexpr (hop::has_tag<OL, tag_ints>::value) {
// ...
}
else
if constexpr (hop::has_tag<OL, tag_doubles>::value) {
// ...
}
}
Instead of using just a single entry-point for the overload-set (or as Quuxplusone called it: "one entry point to rule them all") you can use hop::match_tag_t
to select only allow oveloads with the given tag. In the above case, we would have:
struct tag_ints {};
struct tag_doubles {};
using overloads = hop::ol_list <
hop::tagged_ol<tag_ints, hop::non_empty_pack<int>>,
hop::tagged_ol<tag_doubles, hop::non_empty_pack<double>>
>;
template<typename... Ts,
hop::match_tag_t<overloads, tag_ints, Ts...> = 0
>
void foo(Ts&& ... ts) {
// ...
}
template<typename... Ts,
hop::match_tag_t<overloads, tag_doubles, Ts...> = 0
>
void foo(Ts&& ... ts) {
// ...
}
We can also tag types of an overload. This is useful, when we want to access the argument(s) belonging to a certain type of the overload:
struct tag_ints {};
struct tag_double {};
struct tag_numeric {};
using overloads = hop::ol_list <
hop::tagged_ol<tag_ints, std::string, hop::non_empty_pack<hop::tagged_ty<tag_numeric, int>>>,
hop::tagged_ol<tag_doubles, std::string, hop::non_empty_pack<hop::tagged_ty<tag_numeric, double>>>
>;
template<typename... Ts, hop::enable_test<overloads, Ts...> = 0 >
void foo(Ts&& ... ts) {
using OL = hop::enable_t<overloads, Ts...>;
if constexpr (hop::has_tag<OL, tag_ints>::value) {
auto&& numeric_args = hop::get_tagged_args<OL, tag_numeric>(std::forward<Ts>(ts)...);
// numeric_args is a std::tuple containing all the int args
// ...
}
else
if constexpr (hop::has_tag<OL, tag_doubles>::value) {
auto&& numeric_args = hop::get_tagged_args<OL, tag_numeric>(std::forward<Ts>(ts)...);
// numeric_args is a std::tuple containing all the double args
// ...
}
}
Up to now, we can create non-empty homogeneous overloads for specific types. Let's see what else we can do with hop.
A single overload hop::ol<...>
consists of a list of types that are:
- normal C++ types, like
int
,vector<string>
, user-defined types, which may be qualified. Those types are matched as if they were types of function arguments. hop::repeat<T, min>
,hop::repeat<T, min, max>
at leastmin
(and up tomax
) times the argument-list generated byT
. Ifmax
is not specified, thenrepeat
is unbounded. Multiplerepeat
s (even unbounded) in a single overload are possible! Also all other types/type-constructs afterrepeat
are possible.hop::pack<T>
orhop::non_empty_pack<T>
are aliases forhop::repeat<T, 0>
resp.hop::repeat<T, 1>
.hop::optional<T>
is an alias forhop::repeat<T, 0, 1>
hop::eps
is a typedef forhop::repeat<char, 0, 0>
(it consumes no argument)hop::seq<T1,...,TN>
appends the argument-lists generated byT1
, ... ,TN
hop::alt<T1,...,TN>
generates the argument-lists forT1
, ... ,TN
and handles each as a separate casehop::cpp_defaulted_param<T, _Init = default_init<T>>
creates an argument of typeT
or nothing.hop::cpp_defaulted_param
creates a C++-style defult-param: types following ahop::cpp_defaulted_param
must also be ahop::cpp_defaulted_param
hop::general_defaulted_param<T, _Init = default_init<T>>
creates an argument of typeT
or nothing.hop::general_defaulted_param
can appear in any position of the type-listhop::fwd
is a place holder for a forwarding-reference and accepts any typehop::fwd_if<template<class> class _If>
is a forwarding-reference with SFINAE condition applied to the actual parameter typehop::adapt
adapts an existing function as an overload:hop::adapt<bar>
hop::adapted
can be used to adapt existing overload-sets or templates:void bar(int n, std::string s) { ... } template<class T> auto qux(T&& t, double d, std::string const& s) { ... } struct adapt_qux { template<class... Ts> static decltype(qux(std::declval<Ts>()...)) forward(Ts&&... ts) { return qux(std::forward<Ts>(ts)...); } }; using overloads_t = hop::ol_list < hop::adapt<bar>, hop::adapted<adapt_qux> >; template<typename... Ts, hop::enable_test<overloads_t, Ts...> = 0 > decltype(auto) foo(Ts&& ... ts) { using OL = hop::enable_t<overloads_t, Ts...>; if constexpr (hop::is_adapted_v<OL>) { return hop::forward_adapted<OL>(std::forward<Ts>(ts)...); } }
- for template type deduction there is a global and a local version:
-
the global version corresponds to the usual template type deducing. Let's look a an example:
template<class T1, class T2> using map_alias = std::map<T1, T2>const&; template<class T1, class T2> // !!! class T1 is required using set_alias = std::set<T2>const&; ... hop::ol<hop::deduce<map_alias>, hop::deduce<set_alias>> ... std::map<int, std::string> my_map; std::set<std::string> my_set; foo(my_map, my_set); std::set<double> another_set; foo(my_map, another_set); // error
All arguments specified with
hop::deduce
take part in the global type-deduction, thusfoo
can only be called with a map and a set, where the set-type is the same as the mapped-to-type. Please note, that in the definition of the template-alias forset_alias
the unused template typeclass T1
is required, sinceT1
andT2
are deduced by matchingmap_alias
andset_alias
simultaneously and for all templates in the same order. Template non-type parameters are currently not supported. -
in the local version the types are deduced independently for each argument, for example
template<class T> using map_vector = std::vector<T>const&; ... hop::ol<hop::pack<hop::deduce_local<map_vector>>> ... std::vector<int> v1; std::vector<double> v2; std::vector<std::string> v3; foo(v1, v2, v3);
foo
matches any list ofstd::vector
s. Note, that this cannot be achived with global-deduction as the number of deduced-types is variable.
-
- types can be tagged with
hop::tagged_ty<tag_type, T>
for accessing the arguments of an overload
- finally, the following variations of
hop::ol<...>
:andtemplate<template<class...> class _If, class... _Ty> using ol_if;
allow to specify an additional SFINAE-condition which is applied to the actual parameter type pack. There is also versiontemplate<class _Tag, template<class...> class _If, class... _Ty> using tagged_ol_if;
tagged_ol_if_q
with expects a quoted meta-function as SFINAE-condition.
All overloads for a single function are gathered in a hop::ol_list<...>
The following grammar describes how to build argument-lists for overload-sets:
CppType =
any (possibly cv-qualified) C++ type
Type =
CppType
| tagged_ty<tag, Type>
Argument =
Type
| repeat<Argument, min, max>
| seq<ArgumentList>
| alt<ArgumentList>
| cpp_defaulted_param<Type, init>
| general_defaulted_param<Type, init>
| fwd
| fwd_if<condition>
ArgumentList =
Argument
| ArgumentList, Argument
Inside a function hop
provides several templates and functions for inspecting the current overload and accessing function arguments:
-
get_count...
returns the number of arguments (having a certain tag or satisfying a certain condition)template<class _Overload> constexpr size_t get_count(); template<class _Overload, class _Tag> constexpr size_t get_tagged_count(); template<class _Overload, class _If> constexpr size_t get_count_if_q(); template<class _Overload, template<class> class _If> constexpr size_t get_count_if();
-
get_args...(std::forward<Ts>(ts))...)
returns the arguments (having a certain tag or satisfying a certain condition) as a tuple of referencestemplate<class _Overload, class... Ts> constexpr decltype(auto) get_args(Ts &&... ts); template<class _Overload, class _Tag, class... Ts> constexpr decltype(auto) get_tagged_args(Ts &&... ts); template<class _Overload, class _If, class... Ts> constexpr decltype(auto) get_args_if_q(Ts &&... ts); template<class _Overload, template<class> class _If, class... Ts> constexpr decltype(auto) get_args_if(Ts &&... ts);
-
template<class _Overload, class _Tag, size_t tag_index = 0, class... Ts> constexpr decltype(auto) get_arg(Ts &&... ts);
returns template<class _Overload, class _Tag, class... Ts> constexpr decltype(auto) get_tagged_args(Ts &&... ts);
template<class _Overload, class _If, class... Ts> constexpr decltype(auto) get_args_if_q(Ts &&... ts);
template<class _Overload, template class _If, class... Ts> constexpr decltype(auto) get_args_if(Ts &&... ts);
// get_arg_or_call will always go for the first type with a matching tag template<class _Overload, class _Tag, size_t tag_index = 0, class _FnOr, class... Ts> constexpr decltype(auto) get_arg_or_call(_FnOr&& _fnor, Ts&&... ts) { return impl::get_arg_or<_Overload, _Tag, tag_index, impl::or_behaviour::is_a_callable>(std::forward<_FnOr>(_fnor), std::forward<Ts>(ts)...); } // get_arg_or will always go for the first type with a matching tag template<class _Overload, class _Tag, size_t tag_index = 0, class _Or, class... Ts> constexpr decltype(auto) get_arg_or(_Or && _or, Ts &&... ts) { return impl::get_arg_or<_Overload, _Tag, tag_index, impl::or_behaviour::is_a_value>(std::forward<_Or>(_or), std::forward<Ts>(ts)...); } // get_arg will always go for the first type with a matching tag template<class _Overload, class _Tag, size_t tag_index = 0, class... Ts> constexpr decltype(auto) get_arg(Ts &&... ts) { return impl::get_arg_or<_Overload, _Tag, tag_index, impl::or_behaviour::result_in_compilation_error>(0, std::forward<Ts>(ts)...); }
Examples can be found in test\hop_test.cpp.
hop was the subject of a guest-post at FluentC++ "How to Define A Variadic Number of Arguments of the Same Type – Part 4", released at 07/01/20 (https://www.fluentcpp.com/2020/01/07/how-to-define-a-variadic-number-of-arguments-of-the-same-type-part-4/)