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. Ifmaxis not specified, thenrepeatis unbounded. Multiplerepeats (even unbounded) in a single overload are possible! Also all other types/type-constructs afterrepeatare 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::epsis a typedef forhop::repeat<char, 0, 0>(it consumes no argument)hop::seq<T1,...,TN>appends the argument-lists generated byT1, ... ,TNhop::alt<T1,...,TN>generates the argument-lists forT1, ... ,TNand handles each as a separate casehop::cpp_defaulted_param<T, _Init = default_init<T>>creates an argument of typeTor nothing.hop::cpp_defaulted_paramcreates a C++-style defult-param: types following ahop::cpp_defaulted_parammust also be ahop::cpp_defaulted_paramhop::general_defaulted_param<T, _Init = default_init<T>>creates an argument of typeTor nothing.hop::general_defaulted_paramcan appear in any position of the type-listhop::fwdis 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::adaptadapts an existing function as an overload:hop::adapt<bar>hop::adaptedcan 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); // errorAll arguments specified with
hop::deducetake part in the global type-deduction, thusfoocan 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_aliasthe unused template typeclass T1is required, sinceT1andT2are deduced by matchingmap_aliasandset_aliassimultaneously 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);foomatches any list ofstd::vectors. 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_qwith 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/)