Giter Club home page Giter Club logo

hop's Introduction

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, ...);

hop's People

Contributors

tobias-loew avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

hop's Issues

Minor nits

  • Should #include <limits>, to satisfy GCC.

  • HOP_MAX_DEDUDCABLE_TYPES is misspelled; should be HOP_MAX_DEDUCIBLE_TYPES

  • in the README, intependently is misspelled; should be independently

  • It feels to me like you might want to add something like this match_tag_t https://godbolt.org/z/WMKdfT so that the client can say "I want this function to participate in overload resolution only if it matches this specific pattern out of the overload set I've crafted." All the README examples seem to be focused on making "one entry point to rule them all" and then using if constexpr inside to break out the handling for the different specific patterns.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.