Skip to content

Latest commit

 

History

History
427 lines (346 loc) · 16.4 KB

README.md

File metadata and controls

427 lines (346 loc) · 16.4 KB

hop

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)

come and hop with me!

luna-bunny-wants-to-hop

what is hop

hop is a small library that allows to create proper homogeneous variadic function parameters

what does proper mean

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...

creating overload-sets with hop

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 as decltype(hop::enable<overloads, Ts...>()) and holds the information about the selected overload

while the almost identical

  • hop::enable_test<overloads, Ts...> is defined as decltype((hop::enable<overloads, Ts...>()), 0) and can be used as SFINAE condition (as non-type template parameter of type int, which usually has the default value 0).
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 least min (and up to max) times the argument-list generated by T. If max is not specified, then repeat is unbounded. Multiple repeats (even unbounded) in a single overload are possible! Also all other types/type-constructs after repeat are possible.
  • hop::pack<T> or hop::non_empty_pack<T> are aliases for hop::repeat<T, 0> resp. hop::repeat<T, 1>.
  • hop::optional<T> is an alias for hop::repeat<T, 0, 1>
  • hop::eps is a typedef for hop::repeat<char, 0, 0> (it consumes no argument)
  • hop::seq<T1,...,TN> appends the argument-lists generated by T1, ... , TN
  • hop::alt<T1,...,TN> generates the argument-lists for T1, ... , TN and handles each as a separate case
  • hop::cpp_defaulted_param<T, _Init = default_init<T>> creates an argument of type T or nothing. hop::cpp_defaulted_param creates a C++-style defult-param: types following a hop::cpp_defaulted_param must also be a hop::cpp_defaulted_param
  • hop::general_defaulted_param<T, _Init = default_init<T>> creates an argument of type T or nothing. hop::general_defaulted_param can appear in any position of the type-list
  • hop::fwd is a place holder for a forwarding-reference and accepts any type
  • hop::fwd_if<template<class> class _If> is a forwarding-reference with SFINAE condition applied to the actual parameter type
  • hop::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, thus foo 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 for set_alias the unused template type class T1 is required, since T1 and T2 are deduced by matching map_alias and set_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 of std::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<...>:
      template<template<class...> class _If, class... _Ty>
      using ol_if;
    
    and
      template<class _Tag, template<class...> class _If, class... _Ty>
      using tagged_ol_if;
    
    allow to specify an additional SFINAE-condition which is applied to the actual parameter type pack. There is also version 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 references

      template<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 on FluentC++

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/)

this library is presented to you by the hop-experts
Luna & Rolf

hop-experts

that's one small step for man, a lot of hops for a bunny!

luna-bunny bunny(hop, hop, hop, ...);