如何对通用列表提取进行元编程以构建函数调用

How to metaprogram a generic list extraction for building a function call

我有一个 类 家族,其方法具有以下签名:

double compute(list<T> pars)

此方法使用通过 pars 接收到的参数执行计算。对于每个 compute(list) 方法,我都有另一个 compute(x1, x2, ..., xn) 方法,它是实现实际计算的方法。因此,compute(pars) 应该做一些诸如:

double compute(list<T> pars)
{
  T x1 = list.pop_back();
  T x2 = list.pop_back();
  // .. so on until last parameter xn
  T xn = list.pop_back();

  return compute(x1, x2, .., xn); // here the real implementation is called
}

这个模式重复了很多次,唯一可以改变的是 pars 列表的大小,当然还有 compute(x1, x1, ..).

的实现

我想为"driying"这个重复的过程找到一个方法;具体来说,提取 pars 列表中的参数并构建对 compute(x1, x2, .., xn) 的调用。我一直在尝试做一些宏观技巧但没有成功。

我的问题是它是否存在某种基于元编程的方式允许我实现 compute(list<T> pars) 一次并简单地重用它 n 以便执行对 compute(x1, x2, ..., xn)

的调用

EDIT: 这是另一个compute(x1, ...)

的签名
VtlQuantity compute(const VtlQuantity & x1, 
                    const VtlQuantity & x2,
                    // any number of pars according the class
                    const VtlQuantity & xn) const

'VtlQuantityis a class representingdouble`'s,它们的单位和其他东西。

下面的 C++17 解决方案。 wandbox link

(由于 Jarod42 大大简化)

  • 假设参数的数量 N 在编译时已知,但列表可以有任意大小。

  • 如示例所示多次调用pop_back(),然后调用一个函数。


template <typename T>
struct list
{
    T pop_back() { return T{}; }
};

namespace impl
{    
    template<typename TList, std::size_t... TIs>
    auto list_to_tuple(TList& l, std::index_sequence<TIs...>)
    {
        using my_tuple = decltype(std::make_tuple((TIs, l.pop_back())...));
        return my_tuple{((void)TIs, l.pop_back())...};
    }
}

template<std::size_t TN, typename TList>
auto list_to_tuple(TList& l)
{
    return impl::list_to_tuple(l, std::make_index_sequence<TN>());
}

template <std::size_t TN, typename TList, typename TF>
auto call_with_list(TList& l, TF&& f)
{
    return std::experimental::apply(f, list_to_tuple<TN>(l));
}

void test_compute(int, int, int)
{
    // ...
}

int main()
{
    list<int> l{};
    call_with_list<3>(l, test_compute);
}

How does it work?

这个想法是我们 "convert" 一个列表到一个元组,使用 list_to_tuple<N>(list).

指定我们想要在编译时从列表中弹出多少元素

从列表中获取元组后,我们可以使用 std::experimental::apply 将元组的元素作为参数来调用函数:这是通过 call_with_list<N>(list, func).

完成的

要从列表创建元组,需要做两件事:

  1. 正在创建 std::tuple<T, T, T, T, ...>,其中 T 重复 N 次。

  2. 调用 list<T>::pop_back() N 次,将项目放入元组中。

为了解决第一个问题,decltype 用于获取以下可变参数扩展的类型:std::make_tuple((TIs, l.pop_back())...)。使用逗号运算符,以便 TIs, l.pop_back() 计算为 decltype(l.pop_back()).

为了解决第二个问题,在 std::initializer_list 元组构造函数中使用了可变参数扩展,它保证了求值顺序:return my_tuple{((void)TIs, l.pop_back())...};。此处使用与上述相同的逗号运算符 "trick"。


Can I write it in C++11?

是的,但会稍微多一些"annoying"。

  • std::experimental::apply 不可用:在线查找解决方案 like this one.

  • std::index_sequence 不可用:您必须自己实施。

您可以执行以下操作:

template <typename Func, typename T, std::size_t ... Is>
decltype(auto) apply(Func&& f, const std::list<T>& pars, std::index_sequence<Is...>)
{
    std::vector<T> v(pars.rbegin(), pars.rend());

    return std::forward<Func>(f)(v.at(Is)...);
}

template <std::size_t N, typename Func, typename T>
decltype(auto) apply(Func&& f, const std::list<T>& pars)
{
    return apply(std::forward<Func>(f), pars, std::make_index_sequence<N>());
}

用法类似于:

apply<6>(print, l);

Demo

要自动计算函数的元数,您可以创建一个特征:

template <typename F> struct arity;

template <typename Ret, typename ...Args> struct arity<Ret(Args...)>
{
    static constexpr std::size_t value = sizeof...(Args);
};

然后

template <typename Func, typename T>
decltype(auto) apply(Func&& f, const std::list<T>& pars)
{
    constexpr std::size_t N = arity<std::remove_pointer_t<std::decay_t<Func>>>::value;
    return apply(std::forward<Func>(f), pars, std::make_index_sequence<N>());
}

Demo

您必须丰富 arity 以支持 Functor(作为 lambda)。

template<class T> using void_t = void;

template<class T, class F, std::size_t N=0, class=void>
struct arity:arity<T, F, N+1> {};

template<class F, class T, class Indexes>
struct nary_result_of{};

template<std::size_t, class T>
using ith_T=T;

template<class F, class T, std::size_t...Is>
struct nary_result_of<F, T, std::index_sequence<Is...>>:
  std::result_of<F( ith_T<Is, T> )>
{};

template<class T, class F, std::size_t N>
struct arity<T, F, N, void_t<
  typename nary_result_of<F, T, std::make_index_sequence<N>>::type
>>:
  std::integral_constant<std::size_t, N>
{};

arity 使用了一个 C++14 特性(索引序列,易于在 C++11 中编写)。

它采用类型 FT 并告诉您可以传递给 F 的最少 T 数量以使调用有效。如果没有 T 个符合条件,它会破坏您的模板实例化堆栈,并且您的编译器会抱怨或死掉。

template<class T>
using strip = typename std::remove_reference<typename std::remove_cv<T>::type>::type;

namespace details {
  template<class T, std::size_t N, class F, class R,
    std::size_t...Is
  >
  auto compute( std::index_sequence<Is...>, F&& f, R&& r ) {
    std::array<T, N> buff={{
      (void(Is), r.pop_back())...
    }};
    return std::forward<F>(f)( buff[Is]... );
  }
}
template<class F, class R,
  class T=strip< decltype( *std::declval<R&>().begin() ) >
>
auto compute( F&& f, R&& r ) {
  return details::compute( std::make_index_sequence<arity<F,T>{}>{}, std::forward<F>(f), std::forward<R>(r) );
}

转换为 C++11 唯一真正烦人的是 auto return 类型 compute。我必须重写我的 arity.

这个版本应该自动检测非函数指针的数量,让你用 lambdas 或 std::functions 或你有什么来调用它。

这是针对更一般的应用问题类型的 C++11 解决方案 函数或仿函数 F,采用 N 类型 T 参数并返回类型 RetN 参数 在某些输入迭代器的连续位置。

与由参数的某些容器参数化的解决方案相比,这获得了一些灵活性:-

  • 您可以从序列中任意 N 大小的范围中提取参数。

  • 序列不必是 T 的容器 - 尽管它必须是可转换为 T.

    [=99 的序列=]
  • 您可以从后到前(就像您所做的那样)或从前到后提取参数, 来自标准容器类型或任何支持正向和反向迭代器的类型。

  • 您甚至可以将 F 应用于直接从某些输入流中使用的参数,而无需 中间提取.

  • 当然你可以改变你对序列类型的看法 无需更改功能应用程序解决方案即可提供参数。

界面

template<typename Func, typename InIter, typename Stop = std::nullptr_t>
typename function_traits<typename std::decay<Func>::type>::return_type
invoke(Func && f, InIter it, Stop stop = Stop());

您可以这样使用:

auto result = invoke(func,iter);

func应用于迭代器连续N个位置的参数 iter.

这样,您就不会对 N 参数是否可以合法访问进行范围检查 在那些位置上你的程序。您将发现的范围检查代码 在实现中将编译为空,如果你越界 会有UB。

如果你想要范围检查,你可以改为代码:

auto result = invoke(func,iter,end);

其中 end 是与 iter 相同类型的迭代器,用于分隔 可用范围以通常的方式。在这种情况下,std::out_of_range 将 如果 N 超出范围的大小,则抛出。

实施

#include <type_traits>
#include <functional>
#include <string>

template<typename T>
struct function_traits;

template <typename Ret, typename ArgT, typename... ArgRest>
struct function_traits<Ret(*)(ArgT, ArgRest...)>
{
    static constexpr std::size_t n_args = 1 + sizeof...(ArgRest);
    using first_arg_type = ArgT;
    using return_type = Ret;
};

template <typename Ret, typename ArgT, typename... ArgRest>
struct function_traits<std::function<Ret(ArgT, ArgRest...)>>
{
    static constexpr std::size_t n_args = 1 + sizeof...(ArgRest);
    using first_arg_type = ArgT;
    using return_type = Ret;
};

namespace detail {

template<typename Left, typename Right>
typename std::enable_if<!std::is_same<Left,Right>::value>::type
range_check(Left, Right, std::string const &){}

template<typename Left, typename Right>
typename std::enable_if<std::is_same<Left,Right>::value>::type
range_check(Left start, Right end, std::string const & gripe) {
    if (start == end) {
        throw std::out_of_range(gripe);
    }
}

template<
    std::size_t N, typename Func, typename InIter, typename Stop, 
    typename ...Ts
>
typename std::enable_if<
    N == function_traits<typename std::decay<Func>::type>::n_args, 
    typename function_traits<typename std::decay<Func>::type>::return_type
>::type
invoke(Func && f, InIter, Stop, Ts...args)
{
    return f(args...);
}

template<
    std::size_t N, typename Func, typename InIter, typename Stop,
    typename ...Ts
>
typename std::enable_if<
    N != function_traits<typename std::decay<Func>::type>::n_args,
    typename function_traits<typename std::decay<Func>::type>::return_type
>::type
invoke(Func && f, InIter it, Stop stop, Ts...args)
{
    range_check(it,stop,
        "Function takes more arguments than are available "
        "in `" + std::string(__PRETTY_FUNCTION__) + '`');
    using arg_type = typename 
        function_traits<typename std::decay<Func>::type>::first_arg_type;
    auto arg = static_cast<arg_type>(*it);
    return invoke<N + 1>(std::forward<Func>(f),++it,stop,args...,arg);
}

} // namespace detail

template<typename Func, typename InIter, typename Stop = std::nullptr_t>
typename function_traits<typename std::decay<Func>::type>::return_type
invoke(Func && f, InIter it, Stop stop = Stop())
{
    return detail::invoke<0>(std::forward<Func>(f),it,stop);
}

所提供的function_traits<T>的两个专业将限制 编译为至少接受一个参数的函数类型 T,这应该 足以满足可能的应用。如果你需要支持 对采用 0 个参数的类型进行调用,然后您可以使用以下方法扩充它们:

template <typename Ret>
struct function_traits<Ret(*)()>
{
    static constexpr std::size_t n_args = 0;
    using return_type = Ret;
};

template <typename Ret>
struct function_traits<std::function<Ret()>>
{
    static constexpr std::size_t n_args = 0;
    using return_type = Ret;
};

免费功能的专业化function_traits<Ret(*)(ArgT, ArgRest...)>, 严格来说是一种多余的便利,因为它们也可以包装在 std::function 对象,因为你必须为任何比自由函数更高级的东西做。

演示

对于练习讨论的功能的程序,您可以附加:

#include <iostream>
#include <list>
#include <vector>
#include <deque>
#include <sstream>
#include <iterator>

struct num
{
    double d;
    explicit operator double() const {
        return d;
    }
};

double add4(double d0, double d1, double d2, double d3)
{
    std::cout << d0 << '+' << d1 << '+' << d2 << '+' << d3 << "\n="; 
    return d0 + d1 + d2 + d3;
}

int multiply2(int i0, int i1)
{
    std::cout << i0 << '*' << i1 << "\n="; 
    return i0 * i1;
}

struct S
{
    int subtract3(int i0, int i1, int i2) const
    {
        std::cout << i0 << '-' << i1 << '-' << i2 << "\n="; 
        return i0 - i1 - i2;
    }
    int compute(std::list<int> const & li) const {
        std::function<int(int,int,int)> bind = [this](int i0, int i1, int i2) {
            return this->subtract3(i0,i1,i2);
        };
        return invoke(bind,li.begin());
    }
};


int main()
{
    std::vector<double> vd{1.0,2.0,3.0,4.0};
    std::vector<double> vdshort{9.0};
    std::list<int> li{5,6,7,8};
    std::deque<num> dn{num{10.0},num{20.0},num{30.0},num{40.0}};
    std::istringstream iss{std::string{"10 9 8"}};
    std::istream_iterator<int> it(iss); 
    std::cout << invoke(add4,vd.rbegin()) << '\n';
    std::cout << invoke(multiply2,li.begin()) << '\n';
    std::cout << invoke(add4,dn.rbegin()) << '\n';   
    std::cout << invoke(multiply2,++it) << '\n';
    S s;
    std::cout << '=' << s.compute(li) << '\n';
    try {
        std::cout << invoke(add4,vdshort.begin(),vdshort.end()) << '\n';
    } catch(std::out_of_range const & gripe) {
        std::cout << "Oops :(\n" << gripe.what() << '\n';
    }

    return 0;
}

案例:

    S s;
    std::cout << '=' << s.compute(li) << '\n';

与您的特定问题特别相关,因为我们在这里调用 S::compute(std::list<int> const & li) 应用另一个非静态方法 S 到列表 li 中传递的参数。见执行 S::compute 如何使用 lambda 方便地绑定 将 S 对象和 S::compute 调用为 std::function 我们可以 传递给 invoke.

Live demo