Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Iteratively filtering arguments matching a predicate at compile-time

Context

Firstly, some context: I'm using an empty struct called nothing to emulate something similar to "regular void" in order to prettify some interfaces that rely on chaining multiple function objects together.

struct nothing { };

Example usage:

when_all([]{ return 0; }, []{ }, []{ return 'a'; })
    .then([](int, char){ }); // result of lambda in the middle ignored

In the above example, what's actually happening is that I'm packaging all the results of the function objects passed to when_all in an std::tuple, converting void to nothing (in this example: std::tuple<int, nothing, char>), then I'm using a helper function called apply_ignoring_nothing that invokes a function object by unpacking an std::tuple, ignoring the elements that are nothing.

auto f_then = [](int, char){ };
auto args = std::tuple{0, nothing{}, 'a'};
apply_ignoring_nothing(f_then, args); // compiles

apply_ignoring_nothing is implemented in terms of call_ignoring_nothing.


Question

I have a function call_ignoring_nothing with the following signature:

template <typename F, typename... Ts>
constexpr decltype(auto) call_ignoring_nothing(F&& f, Ts&&... xs);

This function will invoke f by perfectly-forwarding all xs... for which the compile-time is_nothing_v<T> returns false.

is_nothing_v is defined as follows:

template <typename T>
inline constexpr bool is_nothing_v = std::is_same_v<std::decay_t<T>, nothing>;

The way I implemented call_ignoring_nothing is recursively. The base case only takes f and simply invokes it:

#define FWD(x) ::std::forward<decltype(x)>(x)

template <typename F>
constexpr decltype(auto) call_ignoring_nothing(F&& f)
{
    return returning_nothing_instead_of_void(FWD(f));
}

The recursive case takes f, x, and xs..., and conditionally binds x as one of f's arguments if !is_nothing_v<decltype(f)> through a lambda. It then recurses over call_ignoring_nothing passing the newly-created lambda as f:

template <typename F, typename T, typename... Ts>
constexpr decltype(auto) call_ignoring_nothing(F&& f, T&& x, Ts&&... xs)
{
    return call_ignoring_nothing(
        [&](auto&&... ys) -> decltype(auto) {
            if constexpr(is_nothing_v<T>)
            {
                return FWD(f)(FWD(ys)...);
            }
            else
            {
                return FWD(f)(FWD(x), FWD(ys)...);
            }
        },
        FWD(xs)...);
}

I would like to implement call_ignoring_nothing in an iterative manner, possibly making use of pack expansion to filter out the arguments without recursion.

Is it possible to implement call_ignoring_nothing without recursion? I couldn't think of any technique that allows arguments to be filtered out during pack expansion.

like image 342
Vittorio Romeo Avatar asked Sep 17 '17 17:09

Vittorio Romeo


1 Answers

Not so different from the Griwes suggestion but... I suppose you can use std::apply(), std::tuple_cat(), std::get() and tuples that are empty or with value according the value of is_nothing_v.

I mean... something like [edit: improved with a suggestion from T.C. and an example from the OP itself (Vittorio Romeo)]

template <bool B, typename ... Ts>
constexpr auto pick_if (Ts && ... xs)
 {
   if constexpr ( B ) 
      return std::forward_as_tuple(std::forward<Ts>(xs)...);
   else
      return std::tuple{};
 }

template <typename F, typename ... Ts>
constexpr decltype(auto) call_ignoring_nothing (F && f, Ts && ... xs)
 {
   return std::apply(f,
      std::tuple_cat(pick_if<!is_nothing_v<Ts>>(std::forward<Ts>(xs))...)
   );
 }

The following is a working example

#include <tuple>
#include <iostream>
#include <type_traits>

struct nothing { };

template <typename T>
constexpr bool is_nothing_v = std::is_same<std::decay_t<T>, nothing>::value;

template <bool B, typename ... Ts>
constexpr auto pick_if (Ts && ... xs)
 {
   if constexpr ( B )
      return std::forward_as_tuple(std::forward<Ts>(xs)...);
   else
      return std::tuple{};
 }

template <typename F, typename ... Ts>
constexpr decltype(auto) call_ignoring_nothing (F && f, Ts && ... xs)
 {
   return std::apply(f,
      std::tuple_cat(pick_if<!is_nothing_v<Ts>>(std::forward<Ts>(xs))...)
   );
 }

float foo (int a, float b) { return a + b; }

int main ()
 {
   std::cout << call_ignoring_nothing(foo, nothing{}, 12, nothing{},
      2.3f, nothing{}); // print 14.3    
 }

live example on wandbox

like image 152
max66 Avatar answered Oct 27 '22 08:10

max66