嵌套模板函数的重载

Posted

技术标签:

【中文标题】嵌套模板函数的重载【英文标题】:Overload of a nested template function 【发布时间】:2020-09-17 03:35:48 【问题描述】:

我一直在想应该给我的问题加上什么标题,但还是失败了,所以如果你找到了一个好的标题,请编辑它。

我正在尝试为vector 或其他container<T> 编写一个打印函数,并为container<container<T>> 编写另一个打印函数,所以我想出了:

template<typename T>
void print(T const& cont)
    for (const auto& i : cont) 
        cout << i << " ";
    
    cout << endl;


template<typename T, template<typename> typename Cont>
void print(Cont<T> const& cont) 
    for (const auto& i : cont) 
        print(i);
    

我在这里有 2 个目标容器:

vector<vector<int>> subsets;
vector<int> subset;

当我调用print(subset); 时,程序按预期工作,但是当我调用print(subsets) 时,编译器开始抱怨:

error C2679: binary '<<': no operator found which takes a right-hand operand of type 'const std::vector<int,std::allocator<int>>' (or there is no acceptable conversion)

我的结论是它仍在尝试调用非嵌套模板打印函数,并且在我尝试计算向量时在 cout 上失败。

谁能解释为什么重载解决方案没有按我的预期工作以及我在这里做错了什么?即使我将嵌套模板函数重命名为 printn,它也开始抱怨不同的原因:

error C2784: 'void prints(const Cont<T> &)': could not deduce template argument for 'const Cont<T> &' from 'std::vector<std::vector<int,std::allocator<int>>,std::allocator<std::vector<int,std::allocator<int>>>>'

【问题讨论】:

请附上minimal reproducible example 哦,抱歉,还以为少了点什么;),不过 mcve 会很好 【参考方案1】:

简短、简单且不充分的答案是std::vector 有2 个模板参数。您还应该包括一些间距:

template<class T, class A, template<class, class>class C>
void print(C<T,A> const& cont) 
  std::cout << "[ ";
  bool bFirst = true;
  for (const auto& i : cont) 
    if (!bFirst)
      std::cout << ", ";
    bFirst = false;
    print(i);
  
  std::cout << " ]";

所以重载永远不会被调用。

一旦你这样做了,你的代码就不能工作了,因为你没有元素打印机。所以用元素打印机替换你的其他循环打印机:

template<typename T>
void print(T const& i)
  std::cout << i;

Live example.

测试代码:

std::vector<int> a=1,2,3;
print(a);
std::cout << "\n";
std::vector<std::vector<int>> b = a, a, a;
print(b);
std::cout << "\n";

输出:

[ 1, 2, 3 ]
[ [ 1, 2, 3 ], [ 1, 2, 3 ], [ 1, 2, 3 ] ]

这还不够,因为如果您想要更严格的通用打印机,您真的应该做一些更有趣的事情来检测“此对象是否可迭代”和“此对象是否类似于元组”。检测Cont&lt;A,B&gt; 模板是一个糟糕的替代品。

Here 是检测某些东西是否可迭代的代码(忽略错误检查的答案,阅读我链接到的那个)。

然后对执行for(:) 循环的print 中的“参数是否可迭代”进行 SFINAE 测试。

接下来您要做的是检测对象是否类似于元组。如果是,您想打印出元组的每个元素。这将为您提供 std::mapstd::unordered_map 支持。请注意,std::array 既类似于元组又可迭代。

这比检测“可迭代”要难一些,而且随着您使用的 C++ 标准的不同而变化更大,因为新版本的 C++ 正在扩展类似元组的特性。你可以偷懒,只检测std::pairstd::tuple,这将覆盖99%的用例。

【讨论】:

【参考方案2】:

您的问题是std::vector 有不止一种模板类型。因此,改用 T 重载。发生这种情况的原因是语言中关于如何在模板模板参数中考虑默认模板参数的歧义。这导致了在 C++17 和 will allow your code to work in a compliant compiler1 中采用的缺陷报告 DR150。要解决此问题,您可以使用可变参数模板模板参数并调整基本情况以仅打印元素,如

template<typename T>
void print(T const& elem)
    cout << elem << " ";


template<template<typename...> typename Cont, typename... Params>
void print(Cont<Params...> const& cont) 
    for (const auto& i : cont) 
        print(i);
    
    cout << endl;


int main()

    vector<vector<int>> subsets1,2,3,4;
    print(subsets);

输出

1 2 
3 4 

1:我必须像在我的示例中那样调整基本情况,因为现在模板 temaplte 版本将调用嵌套向量

【讨论】:

【参考方案3】:

您还可以使用SFINAE(或 C++ 概念,如果您生活在未来)来获得您想要的结果,而无需知道您的传入容器有多少模板参数。这是一个使用尾随返回类型执行 SFINAE 的示例:

#include <iostream>
#include <vector>

template<typename T>
auto print( const T& cont ) -> decltype(std::cout << *begin(cont), void()) 

    for( auto&& i : cont )
        std::cout << i << ' ';
    std::cout << '\n';


template<typename T>
auto print( const T& cont ) -> decltype(begin(*begin(cont)), void())

    for( auto&& i : cont )
        print(i);


int main()

    const auto subset1  = std::vector<int> 1, 2, 3, 4 ;
    const auto subset2 = std::vector<std::vector<int>> 5,6,7, 8,9 ;
    const auto subset3 = std::vector<std::vector<std::vector<int>>> 
         10,20,30, 40,50 , 
         60, 70,80,90, 100,110,120 ,
         200,400,600 
    ;

    print( subset1 );
    print( subset2 );
    print( subset3 );

Coliru 上实时查看它的输出:

1 2 3 4 
5 6 7 
8 9 
10 20 30 
40 50 
60 
70 80 90 
100 110 120 
200 400 600 

请注意第一个函数如何表示它需要能够通过 begin() 函数编写单个元素(由 range-for 循环隐式使用),而第二个函数至少需要通过 begin() 访问的双重嵌套序列。

我自己可能倾向于使用可变参数模板,除非您需要将容器与容器的容器(容器的容器(容器......))分开处理。

【讨论】:

print( std::vector&lt;std::vector&lt;std::vector&lt;int&gt;&gt;&gt; ) 不能使用这种技术。一个天真的解决方案是将第二个cout &lt;&lt; cont[0][0] 替换为print(cont[0]),但随后您需要启用ADL(例如,使用蹦床和标签),否则递归查找中不会发现它。此外,for(:)[0] 是不同的操作,您正在测试您不使用的东西,并使用您不测试的东西。 @Yakk-AdamNevraumont 公平点。我已经更新了我的答案,改为依赖begin()。这能解决您的投诉吗? 感谢@metal 的回答,是否有文章或链接让我了解这种 decltype 的工作原理?我并没有真正遵循语法。 它是旧的std::enable_if 成语的简化版。首先要注意的是,decltype 返回其参数的类型,而不实际运行其中的代码——它只是说,“如果我要运行这段代码,会产生什么类型?”然后它偷偷地使用逗号运算符丢弃第一部分,只是使返回类型始终为void 如果您考虑这样的声明可能会变得更清楚:static_assert( std::is_same&lt; decltype(float, int), int&gt;::value )。它在编译时断言decltype 表达式的类型为intdecltype 正在评估表达式“, ”,逗号运算符丢弃最左边的项目并只保留最后一项。这说明清楚了吗?

以上是关于嵌套模板函数的重载的主要内容,如果未能解决你的问题,请参考以下文章

C++ 函数重载,函数模板和函数模板重载,选择哪一个?

函数模板遇上函数重载

为啥具有“相同签名”的模板和非模板函数重载调用非模板函数?

函数重载与函数模板 - C++

函数模板重载

函数模板遇上函数重载