提问者:小点点

数组索引序列按指针展开vs按数组引用展开


我在这里查看了libstdc++的to_array实现,注意到它们使用了一个巧妙的技巧,通过使用bool模板参数来决定函数是否应该将元素移动或复制到新创建的数组,从而避免为函数编写额外的重载。

我决定玩玩这个把戏,并编写了一些测试代码:

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>{});
}

在处理这个问题时,我偶然发现了一个我最初认为是编译器中的错误,将a参数从t(&a)[...]更改为t(a)[...]产生了相同的汇编代码,但是在我查看了汇编代码中的去角度标识符之后,我得出的结论是确实不是这样,只是稍微更改了调用foo函数的签名。

例如:

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]);
    }
}

印刷

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

在这两种情况下,都生成了相同的程序集代码,但是当使用t(&a)[...]时,函数调用将类似于

void foo(int(&)[4],std::integer_sequence)

其中,由于使用t(a)[...],导致函数调用如下所示

void foo(int*,std::integer_sequence)

不同之处在于第一个参数的签名从对int数组的引用变为对int(也称为int数组)的指针。

我用Clang++11和G++11测试了代码(没有优化),结果是一致的。

我的问题是,当两个选项都产生相同的汇编代码,并按照预期执行时,为什么您会选择其中一个选项而不是另一个选项? 是否存在这样的情况:它们的行为会不同,从而导致使用T(&a)版本的libstdc++?

下面是我的编译器资源管理器会话。


共1个答案

匿名用户

相比之下,有很多模板代码覆盖了一个非常简单的主题。

在以下示例中,您正在通过引用传递数组:

#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;
}

使用特定的t(¶m_name)[SIZE]参数语法。

但是,如果将bazarr参数的类型修改为int(arr)[4],则括号不再具有任何意义,这相当于int arr[4],即尝试按值传递数组。 然而,这是数组到指针的衰减,例如,如果我们试图使用参数,就好像它实际上是范围可转换的,那么Clang甚至会给我们一个非常说明问题的错误消息:

#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;
}

实际上,我们可以将相同的测试应用到您的更复杂的示例:

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
    // ...
}