带有可变参数模板函数的后置返回类型使用decltype。

39

我想写一个简单的加法程序(只是为了好玩),它可以将所有参数相加,并返回适当类型的总和。 目前,我的代码如下:

#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;
}

在GCC 4.5.1上,对于两个参数,例如sum(2, 5.5),似乎可以正常工作并返回7.5。然而,如果有更多的参数,则会出现sum()未定义的错误。但是,如果我像这样声明sum():

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

那么它适用于任意数量的参数,但是sum(2, 5.5)将返回整数7,这不是我所期望的。 当参数大于两个时,我假设decltype()必须进行某种递归才能推断出t + sum(p...)的类型。这是否合法的C++0x?还是decltype()仅适用于非可变声明?如果是这种情况,你会如何编写这样的函数?


1
这是一个有趣的问题。也许你应该在Usenet组comp.std.c++中询问,是否应该在->decltype(expr)中使用这种“递归调用”。 - sellibitze
3
当前的措辞不应该起作用。函数/变量等的声明点在它们的声明符之后。因此,在晚期指定返回类型中的 sum 无法找到正在定义的 sum 模板。 - Johannes Schaub - litb
1
@sillibitze,这是个好点子,但它将取决于模板参数的类型,因为仅在实例化上下文中进行参数依赖查找。如果它们像这里一样是 intdouble,函数模板将无法找到。如果参数中有一个全局声明的类,则会找到全局的 sum。所以这种情况是相当"随机"的,当它找到 "sum" 时,它并不能通用。 - Johannes Schaub - litb
2
@DeagMG:收到。但这有点超出了问题的范围。 - sellibitze
http://gcc.gnu.org/bugzilla/show_bug.cgi?id=44175 - Alex B
显示剩余4条评论
7个回答

23

我认为问题在于变参函数模板只有在指定返回类型后才被视为声明,因此decltype中的sum永远无法引用变参函数模板本身。但我不确定这是GCC的Bug还是C++0x根本不允许这样做。我的猜测是C++0x不允许在->decltype(expr)部分进行"递归"调用。

作为一种解决方法,我们可以使用自定义特性类来避免在->decltype(expr)中进行"递归"调用:

#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... > {};

这样,我们可以用typename sum_type<T,P...>::type替换你程序中的decltype,然后它就可以编译了。 编辑:由于这实际上返回的是decltype((a+b)+c)而不是更接近加法使用方式的decltype(a+(b+c)),所以你可以用以下代码替换最后一个特化版本:
template<class T, class U, class... P> struct sum_type<T,U,P...>
: id<decltype(
      val<T>()
    + val<typename sum_type<U,P...>::type>()
)>{};

确实这个可以工作。但是我不太理解template<class T, class... P> struct sum_type;。它是否只会使用template<class T, class U, class... P>版本? - Maister
@Maister,第一个特化是针对一个参数的,第二个特化是针对至少两个参数的(P可能是一个空参数包)。但Tomaka17的方法似乎也可以工作。不过有一个细微的区别。我的版本会给你decltype((a+b)+c),而Tomaka17的版本会给你decltype(a+(b+c))。如果你使用奇怪的用户定义类型,这可能会有所不同。 - sellibitze
我明白了,让我看看我是否理解正确。因此,每次实例化sum_type时,都会使用template<class T, class...P> sum_type;,但由于有特化的sum_type<T>和sum_type<T, U, P...>,因此将使用这些特化版本,因此实际上没有必要定义template<class T, class...P> struct sum_type;的主体? - Maister
4
decltype实际上是用来替代像这样不自然的构造的吗?我真的希望这只是GCC的一个错误,尽管我使用的是4.5.3版本,但它仍然存在。 - Alex B
2
我已经在Tomaka17的解决方案中发表了评论,我认为你的解决方案存在相同的问题,将decltype( val<const T&>() + val<const U&>() )替换为decltype( std::declval<T>() + std::declval<U>() )应该可以解决这个问题。 - Marti Nito

8

显然,目前不能在递归方式下使用decltype(也许以后会修复)

您可以使用模板结构来确定总和的类型

虽然看起来很丑,但它确实有效

#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;
}

对于非默认可构造类型,上述方法不适用,需要将 typedef decltype(T() + typename TypeOfSum<P...>::type()) 替换为 typedef decltype(std::declval<T>() + std::declval<typename TypeOfSum<P...>::type>()) 以避免问题。 - Marti Nito

8

C++14的解决方案:

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

返回类型将自动推断。

在在线编译器中查看

如果您想支持不同类型的引用,甚至更好:

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

在在线编译器中查看

如果您需要自然序列的求和方式(即(((a+b)+c)+d)而不是(a+(b+(c+d)))),则解决方案更加复杂:

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)... );
}

在在线编译器中查看

这是一个链接,可以在在线编译器中查看。

1
这是不正确的。你应该在这里加上一个尾随返回类型(自C++11以来),或者用decltype(auto)替换auto(自C++14以来)。第一种方法可能更冗长,但出于各种原因更好。 - KeyC0de
@Nikos,我更新了答案,使用了decltype(auto)。 - anton_rh

3

使用C++11的 std::common_type 可以更简单地回答上一个问题:

只需使用以下代码:

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

关于您的可变参数求和函数的返回类型,可以使用std::common_type。关于std::common_type,以下摘自http://en.cppreference.com/w/cpp/types/common_type

对于算术类型,通用类型也可以看作是(可能是混合模式的)算术表达式的类型,例如T0() + T1() + ... + Tn()。

但显然,这仅适用于算术表达式,并不能解决通用问题。


虽然这可能有效,但它存在以下问题:假设您想计算可能不同的表达式模板的总和。使用您的方法,head + sum(tail...)将无法从表达式模板可以提供的优化中受益! - Marti Nito
@MartiNito:我不太明白你的意思。首先,返回类型是无关优化的(只要它正确,即不涉及昂贵的转换等)。其次,std::common_type<T...> 确定了所有 T... 可以隐式转换的类型。因此,你是否可以在表达式模板上使用它取决于你如何定义这些隐式转换。但我认为对于表达式模板,通常会通过模板递归明确定义返回类型(就像其他答案中一样)。 - davidhigh
假设您使用向量V、矩阵M和标量s进行工作。在求和a1+a2+a3中,其中a1 = sV,a2 = MV和a3 = M.col(1),每个加数都将具有不同的类型(反映底层操作)。将评估延迟并在所需的求和循环中执行它们是有利的。如果a1和a2的common_type是一个已评估的向量,则sum(a1+a2,a3)将循环两次,一次为a1+a2,一次为(a1+a2)+a3。当然,适当地专门化common_type以反映操作a1+a2+a3的表达式模板就可以解决问题了。希望对您有所帮助。 - Marti Nito

2
我提供对已接受答案的改进。只需要两个结构体。
#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;
};

现在只需将您的函数声明为:
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...);
}

有了这个,你的测试代码现在可以工作了。

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

146.5


只是出于好奇,为什么这里的顺序如此重要?我尝试编写了一个简单的示例,并意识到如果交换两个结构定义的顺序,它就无法编译。(换句话说,如果您将“template <typename P>”结构定义放在“template <typename P,typename ... Ps>”结构定义之前。) - tjwrona1992
第二个结构体定义是第一个的特化版本。请参阅partial template specialization - smac89

0

正确的做法:

#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

这完全保留了操作的结果类型(甚至是右值引用)。操作的顺序是自然的:(((a+b)+c)+d)


-1

对于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;
}

阅读有关折叠表达式的内容。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接