In a template argument, what rules allow the compiler to infer the number of items of an array?

Viewed 124

Given the two programs bellow -- identical except for the definition of the template function 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;
}

When using g++, the first program compiles and runs as expected. But the second does not compile. Here is the diagnostic:

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;

Why is the compiler able to infer the number of items in the array in the first case but not in the second? What makes, in the C++11 or C++17 standard, mandatory to write T (&v)[n] instead of T v[n]?

3 Answers

In this:

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

v is a reference to a T[n] array. The size of the array (n) is part of the array's type. So n can be deduced from any fixed-sized array that is passed in.

In this:

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

In a function parameter T v[n], n is ignored, and T v[] is just syntax sugar for T *v (you can see that in the error message - std::size_t len(T*)). So, there is effectively nothing to deduce n from, even if a fixed-length array were passed in.

Template argument deduction is covered by [temp.deduct] [emphasis mine]:

/1 When a function template specialization is referenced, all of the template arguments shall have values. The values can be explicitly specified or, in some cases, be deduced from the use or obtained from default template-arguments.

/2 (... regarding explicit template argument list: not relevant here)

/3 After this substitution is performed, the function parameter type adjustments described in [dcl.fct] are performed. [ Example: A parameter type of “void (const int, int[5])” becomes “void()(int,int)”. — end example ] [...]

Note the reference to [dcl.fct] and the related (non-normative) example in /3, showing the adjustment of a value type array function parameter int[N] to int*, meaning a non-type template parameter N is not deducible from arguments passed to a value type array parameter (N is useless/ignored in this context).

Template argument deduction from a function call, particularly, is covered by [temp.deduct.call]; and the relevant section that differentiates your two examples is [temp.deduct.call]/2.1, which says that if the argument of a call (denoted A) is an array type, and the parameter type (denoted P) is not a reference type, a pointer type is used for type deduction:

/2 If P is not a reference type:

  • (2.1) If A is an array type, the pointer type produced by the array-to-pointer standard conversion is used in place of A for type deduction; otherwise, [...]

Whereas when P is a reference type, as in the following example:

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

[temp.deduct.call]/2.1 does not apply, and for the following call to a function named len with an array as argument

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

name lookup will find the function template len, and template argument deduction will subsequently be applied with P as T[N] (as per [temp.deduct.call]/3) and A as int[3] for the single function parameter of len, which deduces T to int and N to 3 for the complete type deduction of the parameter type P; as per [temp.deduct.type]/1:

Template arguments can be deduced in several different contexts, but in each case a type that is specified in terms of template parameters (call it P) is compared with an actual type (call it A), and an attempt is made to find template argument values (a type for a type parameter, a value for a non-type parameter, or a template for a template parameter) that will make P, after substitution of the deduced values (call it the deduced A), compatible with A.

I.e., the non-type template parameter N is part of the type of the single function parameter (and so is the type template parameter T).

The first one is equivalent to a pointer. See dcl.fct (invoked by temp.deduct as @dfrib's answer explains):

The type of a function is determined using the following rules. ... After determining the type of each parameter, any parameter of type “array of T” ... is adjusted to be “pointer to T”. ...

This does not apply to the second one because it is a reference to an array (there aren't arrays of references, but references to arrays are fine).

Related