trailing return type using decltype with a variadic template function
Asked Answered
L

7

39

I want to write a simple adder (for giggles) that adds up every argument and returns a sum with appropriate type. Currently, I've got this:

#include <iostream>
using namespace std;

template <class T>
T sum(const T& in)
{
   return in;
}

template <class T, class... P>
auto sum(const T& t, const P&... p) -> decltype(t + sum(p...))
{
   return t + sum(p...);
}

int main()
{
   cout << sum(5, 10.0, 22.2) << endl;
}

On GCC 4.5.1 this seems to work just fine for 2 arguments e.g. sum(2, 5.5) returns with 7.5. However, with more arguments than this, I get errors that sum() is simply not defined yet. If I declare sum() like this however:

template <class T, class P...>
T sum(const T& t, const P&... p);

Then it works for any number of arguments, but sum(2, 5.5) would return integer 7, which is not what I would expect. With more than two arguments I assume that decltype() would have to do some sort of recursion to be able to deduce the type of t + sum(p...). Is this legal C++0x? or does decltype() only work with non-variadic declarations? If that is the case, how would you write such a function?

Lepidus answered 19/9, 2010 at 3:15 Comment(9)
This is an interesting problem. Maybe you should ask in the Usenet group comp.std.c++ whether this kind of "recursive call" in ->decltype(expr) is supposed to work or not.Procuration
It's not supposed to work by the current wording. The point of declaration of functions/variables etc.. is after their declarator. Thus, sum in the late specified return type cannot find the sum template being defined.Marquesan
@Johannes: But isn't lookup simply delayed (until the 2nd phase) due to the expression's dependence on template parameters?Procuration
@Procuration that's a good point, but it will depend on the types of the template parameters, because only argument dependent lookup is done at the instantiation context. If they are int and double like here, the function template won't be found. If there is a globally declared class among the arguments, the global sum will be found. So this is rather "random" when it finds the "sum", it doesn't work in general.Marquesan
You forgot to make sum handle rvalue references correctly.Prent
@DeadMG, I don't see a problem here w.r.t. rvalues. Good old references-to-const handle rvalues just fine.Procuration
@sellibitze: Not if you don't forward them. Then, you will invoke the copy constructor and other lvalue semantics, even though in C++0x you could invoke move semantics and save a bunch of performance. Imagine if his sum were invoked on strings. Then he will waste a ton of performance with lvalues.Prent
@DeagMG: Point taken. But this is a little bit outside the scope of the question.Procuration
gcc.gnu.org/bugzilla/show_bug.cgi?id=44175Hydrometer
P
23

I think the problem is that the variadic function template is only considered declared after you specified its return type so that sum in decltype can never refer to the variadic function template itself. But I'm not sure whether this is a GCC bug or C++0x simply doesn't allow this. My guess is that C++0x doesn't allow a "recursive" call in the ->decltype(expr) part.

As a workaround we can avoid this "recursive" call in ->decltype(expr) with a custom traits class:

#include <iostream>
#include <type_traits>
using namespace std;

template<class T> typename std::add_rvalue_reference<T>::type val();

template<class T> struct id{typedef T type;};

template<class T, class... P> struct sum_type;
template<class T> struct sum_type<T> : id<T> {};
template<class T, class U, class... P> struct sum_type<T,U,P...>
: sum_type< decltype( val<const T&>() + val<const U&>() ), P... > {};

This way, we can replace decltype in your program with typename sum_type<T,P...>::type and it will compile.

Edit: Since this actually returns decltype((a+b)+c) instead of decltype(a+(b+c)) which would be closer to how you use addition, you could replace the last specialization with this:

template<class T, class U, class... P> struct sum_type<T,U,P...>
: id<decltype(
      val<T>()
    + val<typename sum_type<U,P...>::type>()
)>{};
Procuration answered 19/9, 2010 at 6:29 Comment(5)
Indeed this works. I don't quite understand the template<class T, class... P> struct sum_type; though. Will it simply use the template<class T, class U, class... P> version?Lepidus
@Maister, The first specialization is for one argument and the second specialization is for at least two arguments (P might be an empty parameter pack). But Tomaka17's approach seems to work as well. There's one slight difference, though. My version gives you decltype((a+b)+c) while Tomaka17's version gives you decltype(a+(b+c)). In case you work with weird user-defined types this might make a difference.Procuration
I see, let's see if I got that right. So each time sum_type is instantiated, template<class T, class...P> sum_type; is used, but since there are specializations sum_type<T> and sum_type<T, U, P...>, those specializations will be used instead, and thus there is no need to actually define the body of template<class T, class...P> struct sum_type; ?Lepidus
Wasn't decltype actually meant to replace unnatural constructs like this? I really hope it's just a GCC bug, though I'm using 4.5.3 and it's still there.Hydrometer
I already commented in Tomaka17's solution, i think for your solution the same problem exists, replacing decltype( val<const T&>() + val<const U&>() ) with decltype( std::declval<T>() + std::declval<U>() ) should solve the problemChristiniachristis
H
8

Apparently you can't use decltype in a recursive manner (at least for the moment, maybe they'll fix it)

You can use a template structure to determine the type of the sum

It looks ugly but it works

#include <iostream>
using namespace std;


template<typename... T>
struct TypeOfSum;

template<typename T>
struct TypeOfSum<T> {
    typedef T       type;
};

template<typename T, typename... P>
struct TypeOfSum<T,P...> {
    typedef decltype(T() + typename TypeOfSum<P...>::type())        type;
};



template <class T>
T sum(const T& in)
{
   return in;
}

template <class T, class... P>
typename TypeOfSum<T,P...>::type sum(const T& t, const P&... p)
{
   return t + sum(p...);
}

int main()
{
   cout << sum(5, 10.0, 22.2) << endl;
}
Homan answered 19/9, 2010 at 6:28 Comment(1)
For non default constructible types, the above does not work, one needs to replace typedef decltype(T() + typename TypeOfSum<P...>::type()) by typedef decltype(std::declval<T>() + std::declval<typename TypeOfSum<P...>::type>()) to avoid issuesChristiniachristis
I
8

C++14's solution:

template <class T, class... P>
decltype(auto) sum(const T& t, const P&... p){
    return t + sum(p...);
}

Return type is deducted automatically.

See it in online compiler

Or even better if you want to support different types of references:

template <class T, class... P>
decltype(auto) sum(T &&t, P &&...p)
{
   return std::forward<T>(t) + sum(std::forward<P>(p)...);
}

See it in online compiler

If you need a natural order of summation (that is (((a+b)+c)+d) instead of (a+(b+(c+d)))), then the solution is more complex:

template <class A>
decltype(auto) sum(A &&a)
{
    return std::forward<A>(a);
}

template <class A, class B>
decltype(auto) sum(A &&a, B &&b)
{
    return std::forward<A>(a) + std::forward<B>(b);
}

template <class A, class B, class... C>
decltype(auto) sum(A &&a, B &&b, C &&...c)
{
    return sum( sum(std::forward<A>(a), std::forward<B>(b)), std::forward<C>(c)... );
}

See it in online compiler

Impatience answered 14/9, 2014 at 13:18 Comment(2)
That is incorrect. You should either put a trailing return type here (since C++11), or replace auto with decltype(auto) (since C++14). The first method may be more verbose but is better for various reasons.Jointer
@Nikos, I updated the answer to use decltype(auto).Sniper
P
3

Another answer to the last question with less typing by using C++11's std::common_type: Simply use

std::common_type<T, P ...>::type

as return type of your variadic sum.

Regarding std::common_type, here is an excerpt from http://en.cppreference.com/w/cpp/types/common_type:

For arithmetic types, the common type may also be viewed as the type of the (possibly mixed-mode) arithmetic expression such as T0() + T1() + ... + Tn().

But obviously this works only for arithmetic expressions and doesn't cure the general problem.

Paradies answered 14/9, 2014 at 13:49 Comment(3)
While this might work, it poses the following problem: Assume you want to calculate the sum over possibly different expression templates. With your approach, head + sum(tail...) will not benefit from the optimizations that the expression templates can provide!Christiniachristis
@MartiNito: I don't exactly understand what you're meaning. First, the return type is nothing which involves optimizations (as long it is correct, i.e. doesn't involve an expensive conversion or so). Second, std::common_type<T...> determines the type all T... can be implicitly converted to. So whether you can use it on expression templates depends on how you define implicit conversions in these. But I think for expression templates one usually would stick to an explicit determination of the return type via template recursion (as in the other answers).Paradies
Assume you work with vectors V matrices M and scalars s. Then within he sum a1+a2+a3 with a1 = sV, a2 = MV and a3 = M.col(1) each of those summands will a different type (reflecting the underlying operation). It is advantageous to delay the evaluations and perform them in the loop which is required for the sum. If common_type of a1 and a2 is a evaluated vector, then sum(a1+a2,a3) will loop twice, once for a1+a2 and once for (a1+a2)+a3. Of course, proper specialization of common_type to reflect the expression template of the operation a1+a2+a3 will do the trick. HthChristiniachristis
F
2

I provide this improvement to the accepted answer. Just two structs

#include <utility>

template <typename P, typename... Ps>
struct sum_type {
    using type = decltype(std::declval<P>() + std::declval<typename sum_type<Ps...>::type>());
};

template <typename P>
struct sum_type<P> {
    using type = P;
};

Now just declare your functions as

template <class T>
auto sum(const T& in) -> T
{
   return in;
}

template <class P, class ...Ps>
auto sum(const P& t, const Ps&... ps) -> typename sum_type<P, Ps...>::type
{
   return t + sum(ps...);
}

With this, your test code now works

std::cout << sum(5, 10.0, 22.2, 33, 21.3, 55) << std::endl;

146.5

Flex answered 28/1, 2018 at 23:42 Comment(2)
Just a question out of curiosity, why does the order matter so much here? I tried writing a simple example and realized that if you swap the order of the two struct definitions it no longer compiles. (in other words if you put the template <typename P> struct definition before the template <typename P, typename... Ps> struct definition.)Mavis
@tjwrona1992 The second struct definition is a specialization of the first. See partial template specialization.Flex
S
0

Right way to do:

#include <utility>

template <typename... Args>
struct sum_type;

template <typename... Args>
using sum_type_t = typename sum_type<Args...>::type;

template <typename A>
struct sum_type<A> {
    using type = decltype( std::declval<A>() );
};

template <typename A, typename B>
struct sum_type<A, B> {
    using type = decltype( std::declval<A>() + std::declval<B>() );
};

template <typename A, typename B, typename... Args>
struct sum_type<A, B, Args...> {
    using type = sum_type_t< sum_type_t<A, B>, Args... >;
};

template <typename A>
sum_type_t<A> sum(A &&a)
{
    return (std::forward<A>(a));
}

template <typename A, typename B>
sum_type_t<A, B> sum(A &&a, B &&b)
{
    return (std::forward<A>(a) + std::forward<B>(b));
}

template <typename A, typename B, typename... C>
sum_type_t<A, B, C...> sum(A &&a, B &&b, C &&...args)
{
    return sum( sum(std::forward<A>(a), std::forward<B>(b)), std::forward<C>(args)... );
}

https://coliru.stacked-crooked.com/a/a5a0e8019e40b8ba

This completely preserves resulting type of operations (even r-value referenceness). The order of operations is natural: (((a+b)+c)+d).

Sniper answered 11/9, 2020 at 7:55 Comment(0)
N
-1

For C++17:

template <class... P>
auto sum(const P... p){
    return (p + ...);
}

int main()
{
    std::cout << sum(1, 3.5, 5) << std::endl;
    return EXIT_SUCCESS;
}

Read about folding expressions.

Nutbrown answered 4/6, 2020 at 18:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.