'What language rules governs that `T&&` in a templated array-by-&& function argument is *not* a forwarding reference?

A static analysis tool I'm using prompts me that I need to std::forward the argument of the following function down the call chain:

template<typename T, std::size_t size>
constexpr void f(T (&& arr)[size]) { /* linter: use std::forward on 'arr' here */ }

as it identifies the function parameter type as a forwarding reference.

A simple test on several compilers show, however, that this is not a forwarding reference. The following example

void g() {
    int a[3]{1, 2, 3};
    f(a);  // #1
}

is rejected at #1 (DEMO), explicitly pointing our that the function parameter is an rvalue reference:

 error: cannot bind rvalue reference of type 'int (&&)[3]' to lvalue of type 'int [3]'

Question

  • What language rules governs that T&& in a templated array-by-&& function argument as in f above is not a forwarding reference?


Solution 1:[1]

The relevant section is [temp.deduct.call]/1 [emphasis mine]:

Template argument deduction is done by comparing each function template parameter type (call it P) that contains template-parameters that participate in template argument deduction with the type of the corresponding argument of the call (call it A) as described below. If removing references and cv-qualifiers from P gives std::initializer_list<P?> or P?[N] for some P? and N and the argument is a non-empty initializer list ([dcl.init.list]), then deduction is performed instead for each element of the initializer list independently, taking P? as separate function template parameter types P?i and the i:th initializer element as the corresponding argument. [...]

Meaning [temp.deduct.call]/3 does not apply for a P that was originally an array reference type (once it reaches /3, /3 applies per element).

However, if we return to the section above we may note that it comes with the restriction

[...] and the argument is a non-empty initializer list [...]

meaning a call, for OP's example function f, like

f({1, 2, 3});  // [temp.deduct.call]/1 applies

But what about the case where the argument is not an initializer list, but an array?

int arr[3]{1, 2, 3};
f(arr);

[temp.deduct.call]/2 have special rules for arrays, but it does not apply as P is a reference type, meaning we do end up at [temp.deduct.call]/3:

If P is a cv-qualified type, the top-level cv-qualifiers of P's type are ignored for type deduction. If P is a reference type, the type referred to by P is used for type deduction.

A forwarding reference is an rvalue reference to a cv-unqualified template parameter that does not represent a template parameter of a class template (during class template argument deduction ([over.match.class.deduct])). If P is a forwarding reference and the argument is an lvalue, the type “lvalue reference to A” is used in place of A for type deduction.

The function parameter in f, however, is an rvalue reference to T[size], which is not a template parameter. This governs the case for when the argument is not an initializer list (and arguably also that case).

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1