Variadic template recursive return type deduction compilation error

社会主义新天地 提交于 2019-11-30 20:13:13

Here we forward the work to helper types:

namespace details {
  template<class...Ts>
  struct sum_t {};

  template<class T>
  struct sum_t<T> {
    T operator()(T t)const{ return std::forward<T>(t); }
  };

  template<class T, class...Ts>
  struct sum_t<T,Ts...> {
    auto operator()(T t, Ts...ts)const
    -> decltype( std::declval<T>() + sum_t<Ts...>{}(std::declval<Ts>()...) )
    {
      return std::forward<T>(t) + sum_t<Ts...>{}(std::forward<Ts>(ts)...);
    }
  };
}

template<class...Ts>
auto sum(Ts...ts)
-> decltype( details::sum_t<Ts...>{}(std::declval<Ts>()...) )
// -> std::result_of_t<details::sum_t<Ts...>(Ts...)>
// above line is C++14 and cleaner version of previous line
{
  return details::sum_t<Ts...>{}(std::forward<Ts>(ts)...);
}

the basic problem was that a template function cannot see itself when calculating its own return type in a -> decltype clause.

There are a few work arounds. The above should work, because a template class can see other specializations of its partial specialization in its own body. Another approach would be to use Koenig lookup (ADL) to defer the searching for its recursive call until the point of instantiation, where it can find itself. I find that second approach more confusing.

If I was to write my own sum for production, I'd have it optionally take the type I expect it to return, and if it did it would accept a zero length sum (creating a default instance), but not requires that the type be default constructable if I pass 1 or more arguments. But I like over-engineered generic code:

template<class R0=void,class...Ts,class R=std::conditional_t<
  !std::is_same<R0,void>{},
  R0,
  std::result_of_t<details::sum_t<Ts...>(Ts...)>
>>
R sum(Ts...ts)
{
  return details::sum_t<R, Ts...>{}(std::forward<Ts>(ts)...);
}

where I modify sum_t to take the return type as the first parameter:

namespace details {
  template<class R,class...Ts>
  struct sum_t {
    R operator()()const{ return {}; }
  };

  template<class R, class T>
  struct sum_t<R, T> {
    using R0 = std::conditional_t<!std::is_same<R,void>{},R,T>;
    R0 operator()(T t)const{ return std::forward<T>(t); }
  };

  template<class R, class T, class...Ts>
  struct sum_t<R, T,Ts...> {
    using R0 = std::conditional_t<
      !std::is_same<R,void>{},
      R,
      decltype( std::declval<T>() + sum_t<void,Ts...>{}(std::declval<Ts>()...) )
    >;
    R0 operator()(T t, Ts...ts)const
    {
      return std::forward<T>(t) + sum_t<void,Ts...>{}(std::forward<Ts>(ts)...);
    }
  };
}

which makes me want to be able to write "do this sum, but cast each sub-sum to R before continuing" or somesuch.

In C++1z, you'll want to use a fold-expression instead. Being able to set R is still useful, as if you are adding up an expression template, it may only be valid as an expression template until the end of the current scope.

To fix this problem in C++14, you may have to use continuation passing style with the R return value.

We could then fold return type deduction into the game to allow

Matrix m = sum( many_matrices... );

to work in Eigen (for example).

When you first start to write generic code, you have to ask yourself "how deep down the rabbit hole do we want to go?"

Barry

In the interests of completeness, since on this version of a similar question, Yakk posted the template specialization solution that I used in the other one, I will provide the ADL solution that he used there:

namespace N { 
    struct adl {}; 

    template <typename A, typename T>
    T sum(A, T t){ 
        return t;
    }   

    template <typename A, typename T, typename ...U>
    auto sum(A a, T t, U... u) -> decltype(t + sum(a, u...)) {
        return t + sum(a, u...);
    }   
}

template <typename... Args>
auto sum(Args... args) -> decltype(sum(N::adl{}, args...))
{
    return sum(N::adl{}, args...);
}

The reason this works is that the sum used in the trailing-return-type of N::sum is a dependent name, and has the following lookup rules from [temp.dep.res]:

In resolving dependent names, names from the following sources are considered:
(1.1) — Declarations that are visible at the point of definition of the template.
(1.2) — Declarations from namespaces associated with the types of the function arguments both from the instantiation context (14.6.4.1) and from the definition context.

Since lookup includes declarations that are visible at point of definition and the definition context, N::sum can find itself recursively.

However, I agree with Yakk that this approach is more confusing.

Quoted from [basic.scope.pdecl]:

The point of declaration for a name is immediately after its complete declarator (Clause 8) and before its initializer (if any)

The declaration of the second function template is complete after the trailing return type decltype(t + sum(u...)). So, when parsing decltype(t + sum(u...)), the second template is not in scope yet, and the compiler can only see the first template which does not match the call.

One possible fix:

template <typename... T>
struct ReturnType;

template <typename T>
struct ReturnType<T> {
  typedef T Type;
};

template <typename T, typename... U>
struct ReturnType<T, U...> {
  typedef typename ReturnType<U...>::Type Type_;
  typedef decltype(std::declval<T>() + std::declval<Type_>()) Type;
};

template <typename T>
T sum(T t){
    return t;
}

template <typename T, typename ...U>
typename ReturnType<T, U...>::Type sum(T t, U... u) {
    return t + sum(u...);
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!