如何从参数列表中推导出函数对象的返回类型?

时间:2014-10-15 13:12:56

标签: c++ templates visual-c++ projection visual-c++-2013

我试图编写一个可以将vector<T>转换为vector<R>的投影函数。这是一个例子:

auto v = std::vector<int> {1, 2, 3, 4};
auto r1 = select(v, [](int e){return e*e; }); // {1, 4, 9, 16}
auto r2 = select(v, [](int e){return std::to_string(e); }); // {"1", "2", "3", "4"}

首次尝试:

template<typename T, typename R>
std::vector<R> select(std::vector<T> const & c, std::function<R(T)> s)
{
   std::vector<R> v;
   std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
   return v;
}

但是

auto r1 = select(v, [](int e){return e*e; });

我明白了:

  

错误C2660:&#39;选择&#39; :function不带2个参数

我必须明确地呼叫select<int,int>才能工作。我不喜欢这样,因为类型是多余的。

auto r1 = select<int, int>(v, [](int e){return e*e; }); // OK

第二次尝试:

template<typename T, typename R, typename Selector>
std::vector<R> select(std::vector<T> const & c, Selector s)
{
   std::vector<R> v;
   std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
   return v;
}

结果是同样的错误,函数不带2个参数。在这种情况下,我实际上必须提供第三种类型的参数:

auto r1 = select<int, int, std::function<int(int)>>(v, [](int e){return e*e; });

第三次尝试:

template<typename T, typename R, template<typename, typename> class Selector>
std::vector<R> select(std::vector<T> const & c, Selector<T,R> s)
{
   std::vector<R> v;
   std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
   return v;
}

对于

auto r1 = select<int, int, std::function<int(int)>>(v, [](int e){return e*e; });

错误是:

  

&#39;选择&#39; :&#39;选择器&#39;,预期的类模板

的模板参数无效

对于

auto r1 = select(v, [](int e){return e*e; });
  

错误C2660:&#39;选择&#39; :function不带2个参数

(我知道最后两次尝试并不是特别好。)

如何编写此select()模板函数来处理我在开头放入的示例代码?

2 个答案:

答案 0 :(得分:25)

选项#1:

基本decltype()用法:

template <typename T, typename F>
auto select(const std::vector<T>& c, F f)
    -> std::vector<decltype(f(c[0]))>
{
    using R = decltype(f(c[0]));
    std::vector<R> v;
    std::transform(std::begin(c), std::end(c), std::back_inserter(v), f);
    return v;
}

选项#2:

基本std::result_of<T>用法:

template <typename T, typename F, typename R = typename std::result_of<F&(T)>::type>
std::vector<R> select(const std::vector<T>& c, F f)
{
    std::vector<R> v;
    std::transform(std::begin(c), std::end(c), std::back_inserter(v), f);
    return v;
}

选项#3:

高级decltype()用法和完美转发(参见注释*):

template <typename T, typename A, typename F>
auto select(const std::vector<T, A>& c, F&& f)
    -> std::vector<typename std::decay<decltype(std::declval<typename std::decay<F>::type&>()(*c.begin()))>::type>
{
    using R = typename std::decay<decltype(std::declval<typename std::decay<F>::type&>()(*c.begin()))>::type;
    std::vector<R> v;
    std::transform(std::begin(c), std::end(c)
                 , std::back_inserter(v)
                 , std::forward<F>(f));
    return v;
}

选项#4:

高级std::result_of<T>用法和完美转发(参见注释*):

template <typename T, typename A, typename F, typename R = typename std::decay<typename std::result_of<typename std::decay<F>::type&(typename std::vector<T, A>::const_reference)>::type>::type>
std::vector<R> select(const std::vector<T, A>& c, F&& f)
{
    std::vector<R> v;
    std::transform(std::begin(c), std::end(c)
                 , std::back_inserter(v)
                 , std::forward<F>(f));
    return v;
}

*注意:选项#3和#4假设std::transform算法采用函数对象按值,然后将其用作非-const lvalue。这就是人们可以看到这种奇怪的typename std::decay<F>::type&语法的原因。如果函数对象应该在select函数本身内调用,并且结果类型不会被用作容器的模板参数(出于最外层 std::decay<T>,然后获取返回类型的正确且可移植的语法是:

/*#3*/ using R = decltype(std::forward<F>(f)(*c.begin()));

/*#4*/ typename R = typename std::result_of<F&&(typename std::vector<T, A>::const_reference)>::type

答案 1 :(得分:11)

你的第一个问题是你认为lambda是std::functionstd::function和lambda是不相关的类型。 std::function<R(A...)>是一个类型擦除对象,可以转换任何(A)可复制,(B)可销毁和(C)可以使用A...调用并返回与R兼容的类型,并删除有关该类型的所有其他信息。

这意味着它可以使用完全不相关的类型,只要它们通过这些测试即可。

lambda是一个可销毁的匿名类,可以复制(除了C ++ 14,有时也是这样),并且你指定了operator()。这意味着您通常可以将lambda转换为具有兼容签名的std::function

从lambda中推导std::function并不是一个好主意(有办法做到这一点,但它们是坏主意:C ++ 14 auto lambdas打破它们,加上你得到不必要的低效率。)

那么我们如何解决您的问题呢?正如我所看到的,你的问题是获取一个函数对象和一个容器,并推断出在每个元素上应用函数对象后会产生哪种元素transform,因此你可以将结果存储在std::vector

这是最接近问题解决方案的答案:

template<typename T, typename R, typename Selector>
std::vector<R> select(std::vector<T> const & c, Selector s) {
  std::vector<R> v;
  std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
  return v;
}

最简单的方法是按模板顺序交换TR,并让调用者明确地通过R,例如select<double>。这会导致TSelector被推断。这并不理想,但确实做了一些改进。

要获得完整的解决方案,有两种方法可以解决此问题。首先,我们可以更改select以返回带有operator std::vector<R>的临时对象,从而将转换延迟到该点。这是一张不完整的草图:

template<typename T, typename Selector>
struct select_result {
  std::vector<T> const& c;
  Selector s;
  select_result(select_result&&)=default;
  select_result(std::vector<T> const & c_, Selector&& s_):
    c(c_), s(std::forward<Selector>(s_)
  {}
  operator std::vector<R>()&& {
    std::vector<R> v;
    std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
    return v;
  }
};
template<typename T, typename Selector>
select_result<T, Selector> select(std::vector<T> const & c, Selector&& s) {
  return {c, std::forward<Selector>(s)};
}

我还可以提供a slicker version,遗憾地依赖于未定义的行为(函数中本地引用的引用捕获在标准下具有生命周期问题)。

但是这摆脱了auto v = select语法 - 你最终会存储产生结果的东西,而不是结果。

您仍然可以执行std::vector<double> r = select( in_vec, [](int x){return x*1.5;} );并且效果非常好。

基本上我将演绎拆分为两个阶段,一个用于参数,一个用于返回值。

但是,没有必要依赖该解决方案,因为还有其他更直接的方法。

对于第二种方法,我们可以自己推断R

template<typename T, typename Selector>
std::vector<typename std::result_of<Selector(T)>::type>
select(std::vector<T> const & c, Selector s) {
  using R = typename std::result_of<Selector(T)>::type;
  std::vector<R> v;
  std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
  return v;
}

这是一个非常可靠的解决方案。一点清理:

// std::transform takes by-value, then uses an lvalue:
template<class T>
using decayed_lvalue = typename std::decay<T>::type&; 
template<
  typename T, typename A,
  typename Selector,
  typename R=typename std::result_of<decayed_lvalue<Selector>(T)>::type
>
std::vector<R> select(std::vector<T, A> const & c, Selector&& s) {
  std::vector<R> v;
  std::transform(begin(c), end(c), back_inserter(v), std::forward<Selector>(s));
  return v;
}

使这成为一个可用的解决方案。 (将R移至template类型列表,允许其他分配器加入vector,删除一些不必要的std::,并在Selector上完成转发。< / p>

然而,我们可以做得更好。

输入为vector这一事实毫无意义:

template<
  typename Range,
  typename Selector,
  typename R=typename std::result_of<Selector(T)>::type
>
std::vector<R> select(Range&& in, Selector&& s) {
  std::vector<R> v;
  using std::begin; using std::end;
  std::transform(begin(in), end(in), back_inserter(v), std::forward<Selector>(s));
  return v;
}

由于无法确定T而无法编译。所以让我们继续努力:

namespace details {
  namespace adl_aux {
    // a namespace where we can do argument dependent lookup on begin and end
    using std::begin; using std::end;
    // no implementation, just used to help with ADL based decltypes:
    template<class R>
    decltype( begin( std::declval<R>() ) ) adl_begin(R&&);
    template<class R>
    decltype( end( std::declval<R>() ) ) adl_end(R&&);
  }
  // pull them into the details namespace:
  using adl_aux::adl_begin;
  using adl_aux::adl_end;
}
// two aliases.  The first takes a Range or Container, and gives
// you the iterator type:
template<class Range>
using iterator = decltype( details::adl_begin( std::declval<Range&>() ) );
// the second is syntactic sugar on top of `std::iterator_traits`:
template<class Iterator>
using value_type = typename std::iterator_traits<Iterator>::value_type;

为我们提供了iterator<Range>value_type<Iterator>别名。他们一起让我们轻松推断出T

// std::transform takes by-value, then uses an lvalue:
template<class T>
using decayed_lvalue = typename std::decay<T>::type&; 

template<
  typename Range,
  typename Selector,
  typename T=value_type<iterator<Range&>>,
  typename R=typename std::result_of<decayed_lvalue<Selector>(T)>::type
>
std::vector<R> select(Range&& in, Selector&& s) {
  std::vector<R> v;
  using std::begin; using std::end;
  std::transform(begin(in), end(in), back_inserter(v), std::forward<Selector>(s));
  return v;
}

bob is your uncle。 (decayed_lvalue反映了Selector类型如何用于极端情况,而iterator<Range&>反映了我们从Range的左值版本获取迭代器。

在VS2013中,有时上面的decltype混淆了他们拥有的C ++ 11的半实现。用iterator<Range>取代decltype(details::adl_begin(std::declval<Range>()))一样丑陋可以解决这个问题。

// std::transform takes by-value, then uses an lvalue:
template<class T>
using decayed_lvalue = typename std::decay<T>::type&; 

template<
  typename Range,
  typename Selector,
  typename T=value_type<decltype(details::adl_begin(std::declval<Range&>()))>,
  typename R=typename std::result_of<decayed_lvalue<Selector>(T)>::type
>
std::vector<R> select(Range&& in, Selector&& s) {
  std::vector<R> v;
  using std::begin; using std::end;
  std::transform(begin(in), end(in), back_inserter(v), std::forward<Selector>(s));
  return v;
}

生成的函数将采用数组,向量,列表,映射或自定义编写的容器,并将采用任何转换函数,并生成结果类型的向量。

下一步是使转换变得懒惰而不是直接将其放入vector。如果你需要摆脱懒惰的评估,你可以让as_vector取一个范围并将它写出一个向量。但是,这就是编写整个库而不是解决问题。