【问题标题】:How can I let the compiler deduce the return type of a template?如何让编译器推断模板的返回类型?
【发布时间】:2014-08-22 16:29:12
【问题描述】:

我来自 Haskell,目前正在修改 C++11,看看它能做什么。我的一个玩具是一个小模板,它试图模仿 Haskell map 函数,即它需要一个包含 X 值的容器和一个将 X 映射到 Y 的函数,并产生一个包含以下值的容器Y。我知道我可以使用std::transform 轻松做到这一点,但这会破坏乐趣。

现在,我的模板如下所示:

template <typename T, typename U>
void myMap( const T &input,
            U &output,
            std::function<typename U::value_type (typename T::value_type)> f );

现在,我的问题是:是否可以调整签名,而不是通过引用(第二个参数)获取输出容器,而是通过返回值生成一个新容器,但编译器可以推断返回类型?像

template <typename T, typename U>
U myMap( const T &input,
       std::function<typename U::value_type (typename T::value_type)> f );

可惜不能这样称呼

std::vector<int> x = { 1, 2, 3, 4 };
std::list<bool> y = myMap( x, []( int x ) { return x % 2 == 0; } );

...至少 Clang 无法在这里推断出返回类型。

我的一个想法是,鉴于输入容器类型和函数类型是已知的,您可以从中构造输出类型。 IE。像

template <typename C, typename T, typename U>
C<U> myMap( const C<T> &input,
            std::function<U (T)> f );

...但是唉C&lt;U&gt; 似乎甚至不是有效的语法。我想知道我是否只需要正确的decltype 仙尘,就像this question 中的情况一样。

【问题讨论】:

  • 我记得我做了一些与您想要的类似的事情,除了它与std::string 的效果不太好,因为它是std::basic_string&lt;T, U&gt;,并且切换它会导致它像std::basic_string&lt;int, T&gt; 这样的东西。但是,我有一个适用于其他一切的尝试。
  • auto y = map&lt;std::list&gt;(x, [](int x){...}); 可以接受吗?你不能在 C++ 中推断返回类型。
  • 在 C++ 中,我们通常不直接使用容器,而是使用迭代器范围。 Haskell 和 C++ 不能很好地相互转换。对于每种语言,学习它的方式。
  • @n.m.:点了。我知道这并不完全是惯用的,我只是碰巧用它来强调 C++ 的(元)类型系统。 :-)
  • std::vector&lt;int&gt;::rebind&lt;char&gt; 会很有趣......分配器会这样做,为什么不容器呢?

标签: c++ templates c++11


【解决方案1】:

正如我之前所说,我之前已经对所有内容进行了此操作,但它无法与 std::basic_string&lt;T,U&gt;(以及由于使用 std::back_inserter 而导致的 std::set 和朋友)一起工作,因为它只会重新绑定它到std::basic_string&lt;stuff,U&gt; 而不是底层容器。但是请注意,将其扩展为使用 std::basic_string&lt;T, U&gt; 的特殊情况很容易。

我做的第一件事是定义一个function_traits 和一个Rebind 元函数,它将类型从Container&lt;T&gt; 重新绑定到Container&lt;U&gt; 其中U 是被传递函数的结果类型,T 是原始类型。结果类型通过function_traits 元函数找到。您可以在下面看到完整的工作代码:

#include <type_traits>
#include <algorithm>

/* Helpers */
template<typename T>
using Type = typename T::type;

template<typename T>
using Unqualified = Type<std::remove_reference<Type<std::remove_cv<T>>>>;

template<typename Specialization, typename Target>
struct rebind {};

/* Sensible default: assume first parameter is for the target */
template<template<typename...> class Cont, typename T, typename... Ts, typename Target>
struct rebind<Cont<T, Ts...>, Target> {
    using type = Cont<Target, Ts...>;
};

/* Special-case */
template<typename Old, std::size_t N, typename Target>
struct rebind<std::array<Old, N>, Target> {
    using type = std::array<Target, N>;
};

template<typename Specialization, typename Target>
using Rebind = Type<rebind<Specialization, Target>>;

#include <tuple>

template<typename T>
struct function_traits : public function_traits<decltype(&T::operator())> {};

template<typename T, typename R, typename... Args>
struct function_traits<R(T::*)(Args...) const> {

    static constexpr size_t args = sizeof...(Args);

    using result_type = R;
    template<size_t i>
    struct arg {
        using type = typename std::tuple_element<i,std::tuple<Args...>>::type;
    };
};

template<typename T>
using Resultant = typename function_traits<T>::result_type;

template<class Cont, typename Map>
auto map(const Cont& cont, Map&& mapped) -> Rebind<Cont, Resultant<Unqualified<Map>>> {
    Rebind<Cont, Resultant<Unqualified<Map>>> result;
    auto result_iterator = std::back_inserter(result);
    for(const auto& elem : cont) {
        *result_iterator = mapped(elem);
    }
    return result;
}

#include <iostream>

int main() {
    auto i = map(std::vector<int>{1,2,3,4,5,6}, [](int x) { return x % 2 == 0; });
    for(auto&& j : i) {
        std::cout << j << ' ';
    }
}

输出:

0 1 0 1 0 1

Live version 在 Coliru 上

【讨论】:

  • 很好,但它不能比这更简单吗?
  • @aaronman 我之前尝试了几种方法,但这个方法很突出。我确实希望它可以与 std::string 一起使用,如果我尝试一下,我可能会回头看看它。
  • 我想这行得通,但我唯一会考虑这样做的情况是与其他程序员一起参加鸡尾酒会。它肯定会得到一些高尔夫掌声。 ;-)
  • @FrerichRaabe 啊,从那以后我的代码发生了一些变化。在这个答案之后,我将所有内容都调制到我自己的列表理解库中。接受的答案和这个都无法与分配器重新绑定和std::string一起工作,我在认真对待我的图书馆事情时遇到了这两个问题。
【解决方案2】:

您可能正在寻找这种语法

#include <algorithm>
#include <functional>
#include <type_traits>
#include <list>

template 
    <
       template<typename, typename...> class Container, 
       typename InType, 
       typename FuncType, 
       typename... Rest
    >
auto myMap (const Container<InType, Rest...>& container,
            FuncType func) -> 
              Container<decltype(func(std::declval<InType>())), Rest...>
{
    Container<decltype(func(std::declval<InType>())), Rest...> result;
    std::transform(std::begin(container), 
                   std::end(container),
                   std::back_inserter(result), 
                   func);
    return result;
}

虽然我不建议在任何实际项目中使用这种风格的代码。

【讨论】:

  • 我喜欢这个(它是我发布的那个的一个较短的版本)但它不起作用std::array
  • std::back_inserter 不能与 std::array 一起使用,无论如何您都需要对其进行重载。
  • 是的,我忘记了 :)
  • 我建议使用std::result_of 修剪这些decltype(func(...))
【解决方案3】:

使用 C++11 中新的函数声明语法。

template <typename T>
auto myMap(const T &input,
           std::function<typename U::value_type (typename T::value_type)> f ) -> decltype(...);

其中“...”被替换为编译器可以从中评估类型的单个语句。困难的部分是在单个语句中定义返回类型,尽管来自 Haskell,您可能会弄明白。

对于普通的函数声明,编译器无法从参数中推断出返回类型,因为参数还没有被解析。使用这种新语法,您可以使用参数来声明返回类型。

下面是一个例子,你可以让编译器推断出在 C++98 中不可能的普通函数的返回类型。

template <typename T1, typename T2>
auto add(T1 t1, T2 t2) -> decltype(t1+t2)
{ return t1 + t2; }

在 C++14 中,他们添加了一些规则以允许编译器自动推断返回类型,而无需使用 decltype 构造。

【讨论】:

    【解决方案4】:

    n.m. 的答案是我认为正确 的答案,但我不认为嵌套模板对可用性的影响值得正确。

    这是我的尝试:

    template <typename T, typename F, typename C = 
            std::vector<typename std::result_of<F(typename T::value_type)>::type>>
    C myMap(const T &input, F f) {
        C ret;
        for (const auto& x : input) {
            ret.push_back(f(x));
        }
        return ret;
    }
    

    这说明了与您的原始代码的两个区别 - 一个,我没有指定 std::function,因为该类引入了很少需要的开销 - 模板将在您的示例中推断出无法表达的 lambda 类型就好了。二,我指定 std::vector 而不是 std::list 作为默认返回类型(您的示例使用列表)。 vector 是首选的 C++ 容器 - 除非您有非常充分的理由使用由基准测试支持的不同容器,否则请使用它。

    我上述方法的一个缺点是很难或不可能列出作为模板先前参数的推导类型。在我看来,解决这个问题的最简单方法是提示参数:

    template <class T> struct hint {};
    
    template <typename T, typename F, typename C>
    C myMap(const T& input, F f, hint<C> h) {
        return myMap<T, F, C>(input, f);
    }
    

    当然,此时你不妨将输出容器作为输入。

    【讨论】:

      猜你喜欢
      • 2017-01-27
      • 1970-01-01
      • 2018-08-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多