【问题标题】:Apply the first valid function of a set of N functions应用一组 N 个函数中的第一个有效函数
【发布时间】:2017-06-21 05:40:09
【问题描述】:

上一个答案显示了如何根据调用的有效性应用函数:here。但是,它适用于两个功能。我想知道这个概念是否可以使用 C++14 中的智能模板编程技巧推广到 N 函数。

问题如下:

template <std::size_t N, class... X>
/* [Return type] */ apply_on_validity(X&&... x)
{
    // Tricks here
}

// The function would be equivalent to a hypothetical
// template <std::size_t N, class... F, class... Args>
// [Return type] apply_on_validity(F&&... f, Args&&... args)
// where N = sizeof...(F) is the number of functions

在执行方面:

apply_on_validity<3>(f1, f2, f3, a, b, c, d, e);

会:

  • 如果表达式有效则调用f1(a, b, c, d, e),否则调用
  • 如果表达式有效则调用f2(a, b, c, d, e),否则调用
  • 如果表达式有效则调用f3(a, b, c, d, e),否则调用
  • 什么都不做

在所有情况下,它都会返回执行的函数的结果。

在调用端,模板参数3表示已经指定的函数个数。

在 C++14 中是否可行,如果可行,如何实现?

【问题讨论】:

  • 我建议改为构建一个具有所需语义的可调用对象作为更方便的接口:make_first_valid_callable(f1, f2, f3)(a, b, c, d, e);,它不会强制用户手动跟踪函数计数并且更具可组合性。
  • @yurikilochek 这很聪明。爱它!现在的问题是如何使用该语法解决问题...

标签: c++ templates c++14 overloading template-meta-programming


【解决方案1】:

:

#include <tuple>
#include <utility>
#include <type_traits>
#include <cstddef>

template <std::size_t N, std::size_t... Js, typename Args>
auto apply_on_validity_impl(int, std::integral_constant<std::size_t, N>, std::index_sequence<Js...>, Args&& args)
{
    // Nothing here
}

template <std::size_t N, std::size_t I, std::size_t... Js, typename Args>
auto apply_on_validity_impl(int, std::integral_constant<std::size_t, I>, std::index_sequence<Js...>, Args&& args)
    -> decltype(std::get<I>(std::forward<Args>(args))(std::get<Js + N>(std::forward<Args>(args))...))
{
    return std::get<I>(std::forward<Args>(args))(std::get<Js + N>(std::forward<Args>(args))...);
}

template <std::size_t N, std::size_t I, std::size_t... Js, typename Args>
decltype(auto) apply_on_validity_impl(char, std::integral_constant<std::size_t, I>, std::index_sequence<Js...> seq, Args&& args)
{
    return apply_on_validity_impl<N>(0, std::integral_constant<std::size_t, I + 1>{}, seq, std::forward<Args>(args));
}

template <std::size_t N, typename... Args>
decltype(auto) apply_on_validity(Args&&... args)
{
    return apply_on_validity_impl<N>(0, std::integral_constant<std::size_t, 0>{}, std::make_index_sequence<sizeof...(Args) - N>{}, std::forward_as_tuple(std::forward<Args>(args)...));
}

DEMO

:

#include <tuple>
#include <utility>
#include <type_traits>
#include <cstddef>

template <std::size_t N, std::size_t I, std::size_t... Js, typename Args>
decltype(auto) apply_on_validity_impl(std::index_sequence<Js...> seq, Args&& args)
{        
    if constexpr (I == N)
    {
    }
    else if constexpr (std::is_invocable_v<std::tuple_element_t<I, Args>, std::tuple_element_t<Js + N, Args>...>)
    {
        return std::get<I>(std::forward<Args>(args))(std::get<Js + N>(std::forward<Args>(args))...);
    }
    else
    {
        return apply_on_validity_impl<N, I + 1>(seq, std::forward<Args>(args));
    }
}

template <std::size_t N, typename... Args>
decltype(auto) apply_on_validity(Args&&... args)
{
    return apply_on_validity_impl<N, 0>(std::make_index_sequence<sizeof...(Args) - N>{}, std::forward_as_tuple(std::forward<Args>(args)...));
}

DEMO 2

【讨论】:

  • std::remove_reference_t 是多余的。
  • 传递这 1000 个 lambda,它会中断。 ;)(递归模板实例化限制)
【解决方案2】:

这是另一个有趣的踢球,滥用重载决议。我们将把每个函数转换成另一个接受rank 参数的函数,将我们所有的函数组合在一起,然后调用它们。选择会自然而然地从重载集中退出。

我们的排名就是这个类型的阶梯:

template <int I> struct rank : rank<I-1> { };
template <> struct rank<0> { };

我们需要一个函数转换器:

template <class T, class F>
auto prepend_arg(F&& f) {
    return [f=std::forward<F>(f)](T, auto&&... args)
        -> decltype(std::forward<F>(f)(std::forward<decltype(args)>(args)...))
    {
        return std::forward<F>(f)(std::forward<decltype(args)>(args)...);
    };
}

然后:

template <std::size_t N, std::size_t... Is, std::size_t... Js,
    class... X>
decltype(auto) apply_impl(std::index_sequence<Is...>,
                          std::index_sequence<Js...>,
                          X&&... x)
{
    auto all = std::forward_as_tuple(std::forward<X>(x)...);

    return overload(
        // all of our function cases
        prepend_arg<rank<N-Is>>(std::get<Is>(all))...,
        // base case: do nothing
        [](rank<0>, auto&&...) {}
    )(rank<N>{}, std::get<N+Js>(all)...);
//               ^^^^^^^^^^^^^^^^^^^^^^^^
//               pass in all the arguments    
}

template <std::size_t N, class... X>
decltype(auto) apply_on_validity(X&&... x) {
    return apply_impl<N>(
        std::make_index_sequence<N>{},
        std::make_index_sequence<sizeof...(X)-N>{},
        std::forward<X>(x)...);
}

我将overload() 留作练习。

【讨论】:

    【解决方案3】:

    您使用的语法有点尴尬,因为您必须确切知道您有多少个函数。结合Piotr Skotnicki's solution,这就解决了这个问题:

    // Would be a local class except for the fact that it needs
    // to have templates, thus can't be local to the function.
    template<class... Fs>
    class apply_first_f final {
    public:
        apply_first_f(Fs&&... fs)
            : fs_{ std::forward<Fs>(fs)... }
        {}
    
        template<class... Args>
        decltype(auto) operator()(Args&&... args) const
        {
            return apply_impl<sizeof...(Args)>(std::make_index_sequence<sizeof...(Fs)>{}, std::forward_as_tuple(std::forward<Args>(args)...));
        }
    private:
        std::tuple<std::decay_t<Fs>...> fs_;
    
        template<size_t argsSize, size_t... Is, class Args>
        decltype(auto) apply_impl(std::index_sequence<Is...>, Args&& args) const
        {
            return apply_on_validity_impl<sizeof...(Fs)>(
                0,
                std::integral_constant<size_t, 0>{},
                std::make_index_sequence<argsSize>{},
                std::tuple_cat(
                    std::forward_as_tuple(std::get<Is>(fs_)...),
                    std::forward<Args>(args)
                )
            );
        }
    };
    
    template<class... Fs>
    auto make_apply_first_valid_f(Fs&&... fs)
    {
        return apply_first_f<Fs...>{ std::forward<Fs>(fs)... };
    }
    

    这个函数可以这样使用:

    make_apply_first_valid_f(f1, f2, f3)(args);
    

    DEMO(改编自 Piotr 的演示)

    将它与 Piotr 的 C++ 1z 示例一起使用只需稍作改动:

    template<class... Fs>
    class apply_first_f final {
        // ...
        template<size_t argsSize, size_t... Is, class Args>
        decltype(auto) apply_impl(std::index_sequence<Is...>, Args&& args) const
        {
            return apply_on_validity_impl<sizeof...(Fs), /* added */ 0>(
                /* 0, */
                /* std::integral_constant<size_t, 0>{}, */
                std::make_index_sequence<argsSize>{},
                std::tuple_cat(
                    std::forward_as_tuple(std::get<Is>(fs_)...),
                    std::forward<Args>(args)
                )
            );
        }
        // ...
    };
    

    【讨论】:

    • 可能要改为std::tuple&lt;std::decay_t&lt;Fs&gt;...&gt; fs_;,以便可以安全地存储对象。
    • @yurikilochek 我唯一想知道的是它是否需要更多传入的函数。
    • 它自然需要复制/移动可构造性。但这几乎不是问题,因为所有使用回调的std 算法、std::bindstd::functionstd::thread 等都需要相同的。
    • 你可能不应该创建可调用类型final,因为这会对像overload 这样你可能想要组合它们的实用程序的幼稚实现造成严重破坏(不好的例子,但我唯一的一个可以临时想到;一些实用程序可能希望从类型中派生出组合问题,并且没有充分的理由禁止它)。
    【解决方案4】:

    我在之前的迭代中已经回答了这个问题,但是 nary 版本有一个错误,并且 clang 有一个内部编译器错误,这使得很难找到这个错误。

    我已经修复了这个错误。所以这里是一堆元编程,后面解决你的问题。

    首先,C++2a 的 is_detected 的自制版本:

    #include <utility>
    #include <type_traits>
    #include <iostream>
    #include <tuple>
    
    namespace details {
      template<class...>using void_t=void;
      template<template<class...>class Z, class=void, class...Ts>
      struct can_apply:std::false_type{};
      template<template<class...>class Z, class...Ts>
      struct can_apply<Z, void_t<Z<Ts...>>, Ts...>:std::true_type{};
    }
    template<template<class...>class Z, class...Ts>
    using can_apply = typename details::can_apply<Z, void, Ts...>::type;
    

    事实上,std::result_of_t 是我们要测试的特征。

    template<class Sig>
    using can_call = can_apply< std::result_of_t, Sig >;
    

    现在 can_call 是 true_type 如果你想要的表达式可以被调用。

    现在我们编写一些编译时调度机制。

    template<std::size_t I>
    using index_t=std::integral_constant<std::size_t, I>;
    template<std::size_t I>
    constexpr index_t<I> index_v{};
    
    constexpr inline index_t<0> dispatch_index() { return {}; }
    template<class B0, class...Bs,
      std::enable_if_t<B0::value, int> =0
    >
    constexpr index_t<0> dispatch_index( B0, Bs... ) { return {}; }
    template<class B0, class...Bs,
      std::enable_if_t<!B0::value, int> =0
    >
    constexpr auto dispatch_index( B0, Bs... ) { 
      return index_v< 1 + dispatch_index( Bs{}...) >;
    }
    
    template<class...Bs>
    auto dispatch( Bs... ) {
      using I = decltype(dispatch_index( Bs{}... ));
      return [](auto&&...args){
        return std::get<I::value>( std::make_tuple(decltype(args)(args)..., [](auto&&...){}) );
      };
    }
    

    dispatch( SomeBools... ) 返回一个 lambda。 SomeBools 中的第一个是编译时真值(具有在布尔上下文中计算为 true 的 ::value)确定返回的 lambda 做什么。称之为调度索引。

    它将 dispatch_index 的参数返回给下一次调用,如果是列表的最后一个,则返回一个空的 lambda。

    现在我们希望能够调度一组可能性。为了简单起见(呵呵),我们将使用index_over

    template<class=void,  std::size_t...Is >
    auto index_over( std::index_sequence<Is...> ){
      return [](auto&&f)->decltype(auto){
        return decltype(f)(f)( std::integral_constant<std::size_t, Is>{}... );
      };
    }
    template<std::size_t N>
    auto index_over(std::integral_constant<std::size_t, N> ={}){
      return index_over(std::make_index_sequence<N>{} );
    }
    

    这让我们无需构建新函数即可扩展参数包。

    那么我们可以将auto_dispatch写成单个函数模板:

    template<class...Fs>
    auto auto_dispatch( Fs&&... fs ) {
      auto indexer =  index_over<sizeof...(fs)>();
      // some compilers dislike lambdas with unexpanded parameter packs.
      // this helps with that:
      auto helper = [&](auto I)->decltype(auto){ 
        return std::get<decltype(I)::value>( std::forward_as_tuple( decltype(fs)(fs)... ) );
      };
      // Get 0 through N-1 as compile-time constants:
      return indexer
      (
        [helper](auto...Is){
          // make tuple of functions:
          auto fs_tuple = std::forward_as_tuple( helper(Is)... );
          // This is what is returned from the `auto_dispatch` function
          // it perfect forwards into the correct lambda passed to `auto_dispatch`
          // based on which is the first one which can be invoked by
          // args...
          return [fs_tuple](auto&&...args) {
            // dispatcher knows which one can be called
            auto dispatcher = dispatch(can_call<Fs(decltype(args)...)>{}...);
            // here we get the first one that can be called, or an empty lambda:
            auto&& f0 = dispatcher(std::get<decltype(Is)::value>(fs_tuple)...);
            // here we do the actual call:
            std::forward<decltype(f0)>(f0)(decltype(args)(args)...);
          };
        }
      );
    }
    

    带有测试代码:

    auto a = [](int x){ std::cout << x << "\n"; };
    auto b = [](std::string y){ std::cout << y << "\n";  };
    struct Foo {};
    auto c = [](Foo){ std::cout << "Foo\n";  };
    int main() {
      auto_dispatch(a, c)( 7 );
      auto_dispatch(a, c)( Foo{} );
      auto_dispatch(a, b, c)( Foo{} );
      auto_dispatch(a, b, c)( "hello world" );
    }
    

    Live example

    上面唯一的 N 元递归模板实例化是 dispatch_index。我可以通过一些工作(分而治之)来获得对数深度。获得恒定的深度是困难的。我会考虑的。

    【讨论】:

      【解决方案5】:

      使用boost::hana::overload_linearly:

      hana::overload_linearly(f1, f2, f3)(a, b, c, d, e)
      

      如果没有一个表达式是有效的,这是一个编译错误,但在这种情况下很容易让它什么都不做:

      hana::overload_linearly(f1, f2, f3, [](auto&&...) {})(a, b, c, d, e)
      

      或者,使用boost::hof::first_of,它做同样的事情

      【讨论】:

        猜你喜欢
        • 2011-07-08
        • 2023-03-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-10-08
        • 1970-01-01
        • 2011-11-05
        相关资源
        最近更新 更多