c++ 具有模板参数pack的递归函数即使存在带结束条件的重载也会继续调用自身

1rhkuytd  于 2023-06-25  发布在  其他
关注(0)|答案(2)|浏览(141)

我试图编写自己的元组,并写了下面的代码:

  1. #include<iostream>
  2. template<typename T>
  3. struct Holder{
  4. T elem;
  5. };
  6. template<typename firstType, typename ...types>
  7. struct scatter : public Holder<firstType>{};
  8. template<typename firstType,typename ...types>
  9. struct myTuple: public scatter<firstType,types...>,public myTuple<types...>{};
  10. template<typename firstType>
  11. struct myTuple<firstType> : public scatter<firstType>{};
  12. template<int>
  13. struct IntToType{};
  14. /* these work ok */
  15. template<typename firstType,typename ...types>
  16. auto getHelper(myTuple<firstType,types...> &tupleObj,IntToType<0>)
  17. -> decltype((tupleObj.scatter<firstType,types...>::elem))
  18. {
  19. return tupleObj.scatter<firstType,types...>::elem;
  20. }
  21. template<typename firstType,typename ...types,int index>
  22. auto getHelper(myTuple<firstType,types...> &tupleObj,IntToType<index>)
  23. -> decltype(getHelper(static_cast<myTuple<types...>&>(tupleObj),IntToType<index-1>()))
  24. {
  25. return getHelper(static_cast<myTuple<types...>&>(tupleObj),IntToType<index-1>());
  26. }
  27. /* these don't work, the first getHelper function doesn't work as an exit */
  28. //template<typename ...types>
  29. //auto getHelper(myTuple<types...> &tupleObj,IntToType<0>)
  30. //-> decltype((tupleObj.scatter<types...>::elem))
  31. //{
  32. // return tupleObj.scatter<types...>::elem;
  33. //}
  34. //
  35. //template<typename ...types,int index>
  36. //auto getHelper(myTuple<types...> &tupleObj,IntToType<index>)
  37. //-> decltype(getHelper(tupleObj,IntToType<index-1>()))
  38. //{
  39. // return getHelper(tupleObj,IntToType<index-1>());
  40. //}
  41. template<int index,typename ...types>
  42. auto get(myTuple<types...> &tupleObj)
  43. -> decltype(getHelper(tupleObj,IntToType<index>()))
  44. {
  45. return getHelper(tupleObj,IntToType<index>());
  46. }
  47. int main(){
  48. myTuple<int,int,float,double> obj;
  49. get<0>(obj) = 2;
  50. get<3>(obj) = 3.5;
  51. std::cout<<get<0>(obj)<<std::endl;
  52. std::cout<<get<3>(obj)<<std::endl;
  53. }

上面的代码运行良好,但是当我用注解中的模板替换两个getHelper函数模板时,递归模板(第二个getHelper)会继续调用自己,编译失败。
有什么问题吗?
我也尝试了下面的例子,它工作得很好:

  1. #include <iostream>
  2. template<int>
  3. struct IntToType{};
  4. template<typename ...types>
  5. struct typelist{};
  6. template<typename ...types>
  7. void test(typelist<types...> &t,IntToType<0>){
  8. std::cout<<"test called with 0"<<std::endl;
  9. }
  10. template<typename ...types,int index>
  11. void test(typelist<types...> &t,IntToType<index>){
  12. std::cout<<"test called with "<<index<<std::endl;
  13. test(t,IntToType<index-1>());
  14. }
  15. int main(){
  16. typelist<int,int,char> obj{};
  17. test(obj,IntToType<2>());
  18. }

所以现在我更困惑了

z2acfund

z2acfund1#

在有效的版本中,我们称之为版本A,您通过执行static_cast<myTuple<types...>&>(tupleObj)删除每个递归调用中的第一个类型,其中tupleObjmyTuple<firstType,types...>。所以firstType被抛弃了。
在失败的版本中,我们称之为版本B,您没有删除任何内容:你直接传递tupleObj,它包含了所有类型。
这很重要,因为函数的签名包含传递给下一个递归调用的类型,在尾部返回类型(-> decltype(...))中。在版本A中,没有问题,因为在某个时候,在参数包types中没有更多的类型,当编译器试图示例化getHelper时,没有留下firstType的类型,它失败了,停止示例化,然后解析调用。在版本B中,参数包types中总是有一些东西,因此编译器一直尝试使用相同的模板参数示例化getHelper,除了索引,索引每次都会减少。

**模板参数推导是在重载解析之前进行的。**在版本B中,重载解析虽然会用IntToType<0>选取正确的重载,但由于模板参数推导卡在了重载解析中,所以无法到达:它尝试示例化无限数量的getHelper函数。

为了让你相信这一点,在版本B中,将主getHelper重载改为:

  1. template<typename ...types,int index>
  2. auto& getHelper(myTuple<types...> &tupleObj,IntToType<index>)
  3. {
  4. return getHelper(tupleObj,IntToType<index-1>());
  5. }

Demo
如果没有尾部返回类型,传递给下一个递归调用的types...就不再是签名的一部分,编译器也不再会在尝试示例化越来越多的东西时陷入困境。请注意,输出可能不是您所期望的,Clang对此给出了有用的警告:该版本没有挑选元组的正确元素。它总是选择第一个(在本例中是int,因此转换警告)。
另一个较小的test示例可以工作,因为递归不是签名的一部分。将主重载更改为此,您将得到相同的问题情况:

  1. template<typename ...types,int index>
  2. auto test(typelist<types...> &t,IntToType<index>) -> decltype(test(t,IntToType<index-1>())) {
  3. std::cout<<"test called with "<<index<<std::endl;
  4. return test(t,IntToType<index-1>());
  5. }

Demo
请注意,另一个test重载是否返回值并不重要,因为同样,编译器甚至不足以进行重载解析。
我个人会继续使用版本A,因为它工作得很好。如果你愿意,你可以通过删除尾部返回类型来简化它:

  1. template<typename firstType,typename ...types>
  2. auto& getHelper(myTuple<firstType,types...> &tupleObj,IntToType<0>)
  3. {
  4. return tupleObj.scatter<firstType,types...>::elem;
  5. }
  6. template<typename firstType,typename ...types,int index>
  7. auto& getHelper(myTuple<firstType,types...> &tupleObj,IntToType<index>)
  8. {
  9. return getHelper(static_cast<myTuple<types...>&>(tupleObj),IntToType<index-1>());
  10. }
  11. template<int index,typename ...types>
  12. auto& get(myTuple<types...> &tupleObj)
  13. {
  14. return getHelper(tupleObj,IntToType<index>());
  15. }

Demo
如果你想使用类似于版本B的东西,你可以修改它,用一个helper函数删除每个递归调用中的第一个类型:

  1. template<typename firstType, typename ...types>
  2. auto myTupleWithoutFirstType(myTuple<firstType, types...>) -> myTuple<types...>;
  3. template<typename ...types,int index>
  4. auto getHelper(myTuple<types...> &tupleObj,IntToType<index>)
  5. -> decltype(getHelper(static_cast<decltype(myTupleWithoutFirstType(myTuple<types...>{}))&>(tupleObj),IntToType<index-1>()))
  6. {
  7. return getHelper(static_cast<decltype(myTupleWithoutFirstType(myTuple<types...>{}))&>(tupleObj),IntToType<index-1>());
  8. }

Demo

展开查看全部
gev0vcfq

gev0vcfq2#

在工作代码中,有两个重载版本的getHelper
1.第一个版本采用myTuple<firstType, types...>IntToType<0>作为参数。它返回tupleObj.scatter<firstType, types...>::elem的类型,这是元组中第一个元素的类型。这个版本作为递归的退出条件。
1.第二个版本采用myTuple<firstType, types...>IntToType<index>作为参数。它递归调用getHelper,并使用index-1和元组中的其余类型。它返回递归调用的结果。此版本处理索引不为零的情况。
第一个版本的模板参数推导工作正常,因为类型myTuple<firstType, types...>在函数签名中显式指定。但是,在注解代码中,getHelper的第一个版本试图直接从tupleObj推导模板参数,这会导致一个问题。由于tupleObj的类型是myTuple<types...>,其中types是一个参数包,因此推导失败,因为无法从参数包推导出模板参数。
在getHelper的第二个版本中,递归调用getHelper(tupleObj, IntToType<index-1>())也无法匹配模板参数,因为模板参数不能从tupleObj推导出来。

相关问题