C++ - 배열 참조

출처

배열 참조는 배열보다 안전한 속도 면에서 유리하다 !!!

int *a[1];   // (1)
int (*a)[1]; // (2)
int &a[1];   // (3)
int (&a)[1]; // (4)

이 4개의 차이점은 ?
(1)은 모두 알다시피 int의 포인터의 배열이다.
(2) int 배열의 포인터이다.
(3)은 참조의 배열로 볼 수 있다. 사실 컴파일 오류이다. 참조의 배열은 규격에서 금지.
(4) 이것이 본 글의 주제인 int의 배열 참조이다.

배열 참조는 그 말 대로이다.
배열과 배열에 대한 참조는 뭐가 다른가?

배열은 함수의 인수로 지정 되었을 때 실제로는 포인터를 지정한 것과 같은 것이다. 이때, 요소 수 정보는 사라진다. 한편 참조형은 포인터가 아니라서, 요수 수에 의미가 있다.
요소 수가 다르면 형태가 다르기 때문이다.

void foo(int a[100]);
void bar(int (&b)[100]);

여기에서 주목하는 것은 foo에서 요소 수 100이라는 정보는 무의미하다는 것이다.
foo는 실제로는 아래와 같은 뜻이다.

void foo(int *a);

그래서 요소 수를 생략해도 컴파일이 된다

void foo(int a[]);

요소 수에 의미가 없다는 것은 아래의 코드도 합법이다.

int c[1];
foo( c );

이것이 위험하다는 것은 알고 있을 것이다.
만약 foo를 다음과 같은 구현하면 어떤 일이 일어날지 아무도 모른다.

void foo(int a[100])
{
    a[10] = 0; // 범위 외 접근은 미정의
}

다른 인수로서 요소 수를ㄹ 넘기는 함수도 많다.
그러나 이는 C 시대의 이야기이다.
C++에는 더 좋은 대안이 제공되고 있다.

int c[1];
bar( c );

이번에는 bar에 c를 넘겨 보면 이것은 컴파일 오류이다.
b는 int(&)[100] 타입이지 포인터가 아니다.
그래서 int[1]의 암묵적 변환을 시도하지만 불가능하다.

int e[100];
int* d = e;
bar( d );

이것도 못한다. int*에서 int(&)[100]에 대한 암묵적 변환을 시도하지만 못한다.

int e[100];
bar( e );

컴파일가능한 건 이것 뿐이다.
앞에 이야기 한대로 배열의 참조는 요소 수에 의미가 있다. 요소 수가 다르면 형이 다른 것이다.

즉 실행 시 범위 체크는 불필요하다. 정적으로 해결할 수 있다.
다른 차이로서는 포인터가 아니기 때문 포인터 연산이 불가능하다.

void bar(int (&b)[100])
{
    b++; // error
}

다음과 같게 하면 배열 타입만 받아 들이고, 요소 수를 정적으로 얻을 수 있다.

template< int i >
void baz(int (&a)[i]);

int c[1];
int e[100];
int* d = c;
baz( c ); // ok
baz( e ); // ok
baz( d ); // error

SFINAE와 조합하면 요소 수 100이하의 배열만 받아 들이는 등의 유연한 처리를 컴파일 시에 할 수 있다.
실행 시 요소 수 조사를 할 필요가 없으므로 속도적으로도 유리한 것이다.
무엇보다 조사를 잊어버리는 실수를 미연에 막을 수 있는 점에서 안전하다.

또한 타입 쓰기가 알기 어렵다면 typedef 하면 된다.

#include <iostream>
     
template<typename T, size_t size>
using array = T[size];
     
void foo( array<int,100>& a ){
    a[0] = 1234;
}
     
template<typename T, size_t size>
constexpr size_t length( array<T,size>& )
{
    return size;
}
     
int main(){
    int a[100];
    foo( a );
    constexpr size_t length = length(a);
    std::cout << length << std::endl;
    std::cout << a[0] << std::endl;
}

본 기억이 있을 것이다.
std::array는 이 연장선상에 있다.
T(&)[n]을 이해하지 못하는 사람들을 위해서 일부러 준비해 준 멋진 타입이다.
모르는 사람은 std::array를 사용하면 된다.

현실에서는 가변 배열을 사용하는 경우가 많을 것이다. 그러나 고정 길이 배열을 취급하는 경우는 적극적으로 참조를 써야 한다.


이 글은 2020-08-03에 작성되었습니다.