2

I was looking at the to_array implementation from libstdc++ here and noticed that they use a clever trick to avoid writing an extra overload for the function by using a bool template argument to decide whether the function should move or copy the elements to the newly created array.

I decided to play around with this trick and wrote some test code:

template <typename ...P>
void dummy(P...) {}

template <typename T>
int bar(T& ref) {
    printf("Copying %d\n", ref);
    return ref;
}

template <typename T>
int bar(T&& ref) {
    printf("Moving %d\n", ref);
    T oldref = ref;
    ref = 0;
    return oldref;
}

template <bool Move, typename T, std::size_t... I>
void foo(T (&a)[sizeof...(I)], std::index_sequence<I...>) {
    if constexpr (Move) {
        dummy(bar(std::move(a[I]))...);
    } else {
        dummy(bar(a[I])...);
    }
}

template <typename T, std::size_t N>
void baz(T (&a)[N]) {
    foo<false>(a, std::make_index_sequence<N>{});
}

template <typename T, std::size_t N>
void baz(T (&&a)[N]) {
    foo<true>(a, std::make_index_sequence<N>{});
}

While messing around with this I stumbled across what I initially thought was a bug in the compiler where changing the a parameter from T(&a)[...] to T(a)[...] yielded the same assembly code, but after I looked at the demangled identifiers in the assembly code I concluded it was indeed not and just changed the signature of the call to the foo function slightly.

For example:

int main() {
    int a1[] = {1, 2, 3, 4};
    baz(a1);
    for (int i = 0; i < 4; i++) {
        printf("%d\n", a1[i]);
    }
    baz(std::move(a1));
    for (int i = 0; i < 4; i++) {
        printf("%d\n", a1[i]);
    }
}

printed

Copying 4
Copying 3
Copying 2
Copying 1
1
2
3
4
Moving 4
Moving 3
Moving 2
Moving 1
0
0
0
0

in both cases and generated the same assembly code, but when using the T(&a)[...] the function call would look like

void foo<false, int, 0ul, 1ul, 2ul, 3ul>(int (&) [4], std::integer_sequence<unsigned long, 0ul, 1ul, 2ul, 3ul>)

where as using T(a)[...] resulted in the function call looking like

void foo<false, int, 0ul, 1ul, 2ul, 3ul>(int*, std::integer_sequence<unsigned long, 0ul, 1ul, 2ul, 3ul>)

the difference being the first parameter's signature changing from a reference to an int array to a pointer to an int (aka an int array).

I tested the code with both clang++11 and g++11 (without optimizations) and the results were consistent.

My question is why you would choose the one option over the other when both produce the same assembly code and does as expected? Is there a case where they would behave differently that lead to libstdc++ using the T(&a) version?

Here is my Compiler Explorer session.

Rikus Honey
  • 534
  • 1
  • 3
  • 17

1 Answers1

2

There's a lot template code covering a, in comparison, quite simple topic.

In the following example, you are passing an array by reference:

#include <iostream>

void baz(int (&arr)[4]) {
    for (const auto e : arr) { std::cout << e << " "; }
}

int main() {
    int a1[] = {1, 2, 3, 4};
    baz(a1);  // 1 2 3 4
    
    return 0;
}

using the particular T (&param_name)[SIZE] parameter syntax.

However, if you modify the type of the arr argument of baz to int (arr)[4], the parantheses no longer have any significance, and this is equivalent to int arr[4], i.e., attempting to pass the array by value. This is, however, array to pointer decay, and e.g. Clang even gives us a very telling error message if we try to use the parameter as if it was actually range-convertible:

#include <iostream>

void baz(int arr[4]) {
    for (const auto e : arr) { std::cout << e << " "; }
}

int main() {
    int a1[] = {1, 2, 3, 4};
    baz(a1);  // error: cannot build range expression with array
              // function parameter 'arr' since parameter with array 
              // type 'int [4]' is treated as pointer type 'int *'
    
    return 0;
}

Indeed, we may apply the same test to your more complex example:

void foo(T (&a)[sizeof...(I)], std::index_sequence<I...>) {
    for (const auto e : a) { (void)e; }  // Ok!
    // ...
}

void foo(T (a)[sizeof...(I)], std::index_sequence<I...>) {
    for (const auto e : a) { (void)e; }
        // Error: invalid range expression of type 'int *'; 
        // no viable 'begin' function available
    // ...
}
dfrib
  • 70,367
  • 12
  • 127
  • 192