在模板参数中,哪些规则允许编译器推断数组的项数?

Posted

技术标签:

【中文标题】在模板参数中,哪些规则允许编译器推断数组的项数?【英文标题】:In a template argument, what rules allow the compiler to infer the number of items of an array? 【发布时间】:2021-04-13 14:42:24 【问题描述】:

鉴于下面的两个程序——除了模板函数len()的定义之外完全相同:

// ================================
//  a.cc
// ================================
#include <iostream>

template<class T, std::size_t n>
std::size_t len(T (&v)[n])  return n; 

int main()

    int arr[] =  1,2,3 ;

    std::cout << len(arr) << std::endl;

// ================================
//  b.cc
// ================================
#include <iostream>

template<class T, std::size_t n>
std::size_t len(T v[n])  return n; 

int main()

    int arr[] =  1,2,3 ;

    std::cout << len(arr) << std::endl;

使用 g++ 时,第一个程序按预期编译和运行。但第二个不编译。这是诊断:

b.cc: In function ‘int main()’:
b.cc:13:25: error: no matching function for call to ‘len(int [3])’
     std::cout << len(arr) << std::endl;
                         ^
b.cc:7:13: note: candidate: template<class T, long unsigned int n> std::size_t len(T*)
 std::size_t len(T v[n])  return n; 
             ^
b.cc:7:13: note:   template argument deduction/substitution failed:
b.cc:13:25: note:   couldn't deduce template parameter ‘n’
     std::cout << len(arr) << std::endl;

为什么编译器能够在第一种情况下推断出数组中的项目数,而在第二种情况下却不能?是什么使得在 C++11 或 C++17 标准中必须编写 T (&amp;v)[n] 而不是 T v[n]

【问题讨论】:

我不打算去寻找参考(因为我觉得这是一个相当众所周知的事实),但是函数参数中的T x[N]完全,完全等同于T *x ... 但T(&amp;x)[N] 不等于T * const (&amp;x) ?!? 不,此规则不适用于递归。它仅适用于数组。 T x[N] 是一个数组,所以它适用于它,T(&amp;x)[N] 是一个引用而不是数组,所以它不适用(对数组的引用,是的,但它的引用无关紧要)。 我想我明白你的意思了。谢谢。 【参考方案1】:

[temp.deduct] [emphasis mine] 涵盖了模板参数推导:

/1 当一个函数模板特化被引用时,所有的 模板参数应该有值。 值可以明确 指定或,在某些情况下,从使用中推断或获得 来自默认模板参数。

/2(...关于显式模板参数列表:此处不相关)

/3 执行完这个替换后,函数参数类型 执行 [dcl.fct] 中描述的调整。 [ 示例:A “void (const int, int[5])”的参数类型变为 “无效()(int,int)”。 —结束示例] [...]

注意/3中对[dcl.fct]和相关(非规范)示例的引用,显示将值类型数组函数参数int[N]调整为int*,表示非类型模板参数N 不能从传递给值类型数组参数的参数中推导出来(N 在这种情况下无用/被忽略)。

从函数调用中推导模板参数,特别是,[temp.deduct.call] 涵盖;区分您的两个示例的相关部分是[temp.deduct.call]/2.1,它表示如果调用的参数(表示为A)是数组类型,并且参数类型(表示为P)是not 引用类型,指针类型用于类型推导:

/2 如果P 不是引用类型:

(2.1) 如果A是数组类型,则使用数组到指针标准转换产生的指针类型代替A的类型 扣除;否则,[...]

而当P 引用类型时,如下例所示:

template<class T, std::size_t n>
std::size_t len(T (&v)[n])  return n; 

[temp.deduct.call]/2.1 不适用,并且对于以下以数组为参数调用名为len 的函数

int arr[] =  1,2,3 ;
(void)len(arr);

名称查找将找到函数模板len,随后将使用P 作为T[N](根据[temp.deduct.call]/3)和A 作为单个函数参数的int[3] 应用模板参数推导的len,推导TintN3,为参数类型P的完整类型推导;根据[temp.deduct.type]/1:

模板参数可以在几种不同的上下文中推导出来,但是 在每种情况下,都是根据模板参数指定的类型 (称为P)与实际类型(称为A)进行比较,然后 尝试查找模板参数值(类型的类型 参数,非类型参数的值,或模板 模板参数),这将使P,在替换后 推导值(称为推导A),兼容A

即非类型模板参数N是单个函数参数的类型的一部分(类型模板参数T也是如此)。

【讨论】:

【参考方案2】:

在这个:

template<class T, std::size_t n>
std::size_t len(T (&v)[n])  return n; 

v 是对T[n] 数组的引用。数组的大小 (n) 是数组类型的一部分。所以n可以从传入的任何固定大小的数组中推导出来。

在这个:

template<class T, std::size_t n>
std::size_t len(T v[n])  return n; 

在函数参数T v[n] 中,n 被忽略,T v[] 只是T *v 的语法糖(您可以在错误消息中看到 - std::size_t len(T*))。因此,即使传入了一个固定长度的数组,实际上也没有什么可以推断出n

【讨论】:

【参考方案3】:

第一个相当于一个指针。请参阅 dcl.fct(由 temp.deduct 调用,正如@dfrib 的回答所解释的那样):

函数的类型使用以下规则确定。 ...确定每个参数的类型后,将任何类型为“T”的参数...调整为“指向T”的指针。 ...

这不适用于第二个,因为它是对数组的引用(没有引用数组,但对数组的引用很好)。

【讨论】:

以上是关于在模板参数中,哪些规则允许编译器推断数组的项数?的主要内容,如果未能解决你的问题,请参考以下文章

C++ Primer 5th笔记(chap 16 模板和泛型编程)模板实参推断和引用

java8的特性都有哪些

C++ Primer 5th笔记(chap 16 模板和泛型编程)模板实参推断

模板与泛型编程——模板实参推断

现代C++之理解auto类型推断

TypeScript类型检查机制