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.