Array Decay to Pointers in Templates

Array decay to pointers in templates

Use the reference type for the parameter

template<typename T> void f(const T& x) 
{
std::cout << sizeof(T);
}

in which case the array type will not decay.

Similarly, you can also prevent decay in your original version of f if you explicitly specify the template agument T as a reference-to-array type

f<int (&)[27]>(array);

In your original code sample, forcing the argument T to have the array type (i.e. non-reference array type, by using typeof or by specifying the type explicitly), will not prevent array type decay. While T itself will stand for array type (as you observed), the parameter x will still be declared as a pointer and sizeof x will still evaluate to pointer size.

why does the array decay to a pointer in a template function

Because arrays can not be passed by value as a function parameter.

When you pass them by value they decay into a pointer.

In this function:

template <class T>
void f(T buff) {

T can not be char (&buff)[3] as this is a reference. The compiler would have tried char (buff)[3] to pass by value but that is not allowed. So to make it work arrays decay to pointers.

Your second function works because here the array is passed by reference:

template <class T>
void f1(T& buff) {

// Here T& => char (&buff)[3]

Why does array type not decay to pointer for class templates?

The "decay" you are talking about is something that happens when deducing a function template parameter from an argument. I have posted a fuller explanation here.

When you explicitly provide a value for a template parameter, there is no deduction step; the value you explicitly provided is exactly the value that the parameter takes.

With class templates, there is never parameter deduction; they must always have their parameters explicitly provided.

Illustrative examples:

template<typename T> void f(T t) {}
template<typename T> struct S { void f(T t) {} };

...

int x[27];
f(x); // Type deduction: decay occurs, T = int *
f<int *>(x); // No deduction. T = int *
f<int[27]>(x); // No deduction. T = int[27]
S<int[27]>().f(x); // No deduction. T = int[27]

In the latter two cases, adjustment still occurs. [temp.deduct]/3 explicitly restates this: when T is an array type, the function parameter T t means that t has a pointer type, in exactly the same fashion that:

void g(int t[27])

actually specifies that t has type int *.

Array reference binding vs. array-to-pointer conversion with templates

The second overload is more specialized than the first one during partial ordering of function templates.

According to [temp.deduct.partial]/5 the reference on T &t of the first overload is ignored during template argument deduction performed for partial ordering. The following paragraphs distinguish based on reference/value category only if both parameters are reference types.

Then T of the first overload can always deduce against a type A* invented from the parameter of the second overload, but T* of the second overload can't deduce against a type A invented from the parameter of the first overload.

Therefore the second overload is more specialized and is chosen.


With T (&t)[4] argument deduction in both directions will fail because deduction of T[4] against A* will fail and so will deduction of T* against A[4]. Array-to-pointer decay of the array type is specified for template argument deduction for a function call but not for template argument deduction for partial ordering. See also active CWG issue 402.

So neither template will be more specialized in this case and the partial ordering tiebreaker does not apply.


The array-to-pointer conversion is not relevant. It is not considered any worse than the identity conversion sequence (see [over.ics.rank]/3.2.1 excluding lvalue transformations which array-to-pointer conversions are).

Why does pointer decay take priority over a deduced template?

The fundamental reason for this (standard-conforming) ambiguity appears to lie within the cost of conversion: Overload resolution tries to minimize the operations performed to convert an argument to the corresponding parameter. An array is effectively the pointer to its first element though, decorated with some compile-time type information. An array-to-pointer conversion doesn't cost more than e.g. saving the address of the array itself, or initializing a reference to it. From that perspective, the ambiguity seems justified, although conceptually it is unintuitive (and may be subpar). In fact, this argumentation applies to all Lvalue Transformations, as suggested by the quote below. Another example:

void g() {}

void f(void(*)()) {}
void f(void(&)()) {}

int main() {
f(g); // Ambiguous
}

The following is obligatory standardese. Functions that are not specializations of some function template are preferred over ones that are if both are otherwise an equally good match (see [over.match.best]/(1.3), (1.6)). In our case, the conversion performed is an array-to-pointer conversion, which is an Lvalue Transformation with Exact Match rank (according to table 12 in [over.ics.user]). [over.ics.rank]/3:

  • Standard conversion sequence S1 is a better conversion sequence than standard conversion sequence S2 if

    • S1 is a proper subsequence of S2 (comparing the conversion sequences in the canonical form defined by 13.3.3.1.1, excluding
      any Lvalue Transformation
      ; the identity conversion sequence is considered to be a subsequence of any non-identity conversion
      sequence) or, if not that,

    • the rank of S1 is better than the rank of S2, or S1 and S2 have the same rank and are distinguishable by the rules in the paragraph below, or, if not that,

    • [..]

The first bullet point excludes our conversion (as it is an Lvalue Transformation). The second one requires a difference in ranks, which isn't present, as both conversions have Exact match rank; The "rules in the paragraph below", i.e. in [over.ics.rank]/4, don't cover array-to-pointer conversions either.

So believe it or not, none of both conversion sequences is better than the other, and thus the char const*-overload is picked.


Possible workaround: Define the second overload as a function template as well, then partial ordering kicks in and selects the first one.

template <typename T>
auto foo(T s)
-> std::enable_if_t<std::is_convertible<T, char const*>{}>
{
std::cout << "raw, size=" << std::strlen(s) << std::endl;
}

Demo.

array decay to pointer and overload resolution

You need to make the first overload a poorer choice when both are viable. Currently they are a tie on conversion ranking (both are "Exact Match"), and then the tie is broken because non-templates are preferred.

This ought to make the conversion ranking worse:

struct stg
{
struct cvt { const char* p; cvt(const char* p_p) : p(p_p) {} };

// matches const char*, but disfavored in overload ranking
stg(cvt c_str); // use c_str.p inside :( Or add an implicit conversion

template<int N>
stg(const char (&str) [N]);
};

What is array to pointer decay?

It's said that arrays "decay" into pointers. A C++ array declared as int numbers [5] cannot be re-pointed, i.e. you can't say numbers = 0x5a5aff23. More importantly the term decay signifies loss of type and dimension; numbers decay into int* by losing the dimension information (count 5) and the type is not int [5] any more. Look here for cases where the decay doesn't happen.

If you're passing an array by value, what you're really doing is copying a pointer - a pointer to the array's first element is copied to the parameter (whose type should also be a pointer the array element's type). This works due to array's decaying nature; once decayed, sizeof no longer gives the complete array's size, because it essentially becomes a pointer. This is why it's preferred (among other reasons) to pass by reference or pointer.

Three ways to pass in an array1:

void by_value(const T* array)   // const T array[] means the same
void by_pointer(const T (*array)[U])
void by_reference(const T (&array)[U])

The last two will give proper sizeof info, while the first one won't since the array argument has decayed to be assigned to the parameter.

1 The constant U should be known at compile-time.

array doesn't decay to pointer if passed by const reference in a template function

Array decay doesn't just happen -- it only happens when the program would fail to compile without it. When you pass an array by reference, there simply is no need for decay to kick in.

Note that the function template can also be written without dividing ugly sizeof expressions:

template <typename T, std::size_t N>
std::size_t size_of_array(T (&array)[N])
{
return N;
}

When a client calls size_of_array, T and N are automatically deduced by the template machinery.

Runtime sized arrays and pointer-decay

During template argument deduction, the array-to-pointer conversion is only used if the function template parameter's type is not a reference.

§14.8.2.1 Deducing template arguments from a function call
[temp.deduct.call]

1 Template argument deduction is done by comparing each function
template parameter type (call it P) with the type of the
corresponding argument of the call (call it A) as described below.
[...]

2 If P is not a reference type:

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


Related Topics



Leave a reply



Submit