🐵

【C++】配列=ポインタではない(Array decayという仕組みについて)

に公開

はじめに

C++を触っていて「配列=ポインタ」だと思い込んでいましたが、調べてみるとどうやら違うようです。調べているとArray decayという仕組みが登場したので、備忘としてまとめます。

そもそも配列とは?

C++における「配列」とは、同じ型の要素が連続してメモリ上に並んでいるデータ構造です。たとえば、int arr[5] = {1, 2, 3, 4, 5};と宣言すれば、arrint型の要素を5つ持つ固定長の配列になります。

C++の配列はコンパイル時にサイズが決まる静的な構造であり、動的にサイズを変更することは基本的にはできません。

ポインタとの違いは?

配列を格納した変数をそのまま出力すると、配列の先頭メモリアドレスが出力されます。

#include <iostream>

int main() {
    int numbers[] = {10, 20, 30, 40, 50};
    std::cout << numbers << std::endl;
}

出力

0x7ffeefbff5a0

これが理由で配列=ポインタと思い込んでいましたが、実際には似て非なるものでした。
違いとしては、配列はサイズに関する情報がコンパイル時に取得できるという点でポインタとは異なります。

たとえばint型の配列とint型のポインタのメモリサイズをそれぞれ出力すると以下のようになります。

#include <iostream>

int main() {
    // 配列のサイズを出力
    int array[] = {1, 2, 3};
    std::cout << "Size of array: " << sizeof(array) << std::endl;

    // ポインタのサイズを出力
    int value = 3;
    int* ptr = &value;
    std::cout << "Size of pointer: " << sizeof(ptr) << std::endl;
};

出力

Size of array: 12
Size of pointer: 8

配列サイズは12byte、ポインタのサイズは8byteになっており、サイズが異なることがわかります。
それぞれ上記のサイズになる理由としては、配列は4byte(int型)の要素が3つで12byte、ポインタは64bitOSのため64bit(=8byte)、となります。
配列はポインタとは異なり、コンパイル時点ではサイズ情報が分かるため、このようにsizeofで配列自体のバイト数を取得することができます。

ちょっと実験

ここで少し実験をしてみます。
内容としては、関数に配列を渡して関数の中でサイズを出力してみます。

#include <iostream>

void print_array_size(const int arr[]) {
    std::cout << "Size of array from function: " << sizeof(arr) << std::endl;
}

int main() {
    int array[] = {1, 2, 3};
    std::cout << "Size of array: " << sizeof(array) << std::endl;

    print_array_size(array);
}

出力[1]

Size of array: 12
Size of array from function: 8

同じ配列に対してサイズを測っているにもかかわらず、異なるサイズが出てきました。

Array decayとは

ここでArray decayという仕組みが登場します。ChatGPT曰く日本語にすると「配列の退化、崩壊、減衰」とかが当てはまるそうですが、検索してもあまりヒットしませんでした。

Array decayはどういうものかというと、配列の変数が自動的にポインタに変換される現象のことを指し、主に関数に配列を引数として渡すときに発生します。

上記の実験ではprint_array_size()に配列が引数として渡されているためそのまま情報が渡っているかと思いきや、実は内部でArray decayが起こっており配列関連の情報が切り落とされています。結果的にポインタとして先頭アドレスの情報のみが渡されて、サイズを出力してもポインタのサイズしか出力されなくなってしまっています。

どのように回避する?

いくつかの方法でArray decayによる配列サイズ情報の損失を回避することができます。おそらく無意識にやっている方も多いのではないかなと思います。

1. 参照とテンプレートを使う

以下のように配列を参照で渡し、サイズ情報をテンプレートで渡すことで、関数内部に配列サイズ情報を渡すことができます。

template<typename T, size_t N>
void print_array_size(T (&arr)[N]) {
    std::cout << "Size of array: " << N << std::endl;
}

2. テンプレートとstd::arrayを使う

配列の代わりにstd::arrayを使うと、Array decayが起こらなくなります。
サイズ情報については1と同様テンプレートで渡します。
注意点としてはarrは値渡しになるため、サイズが大きい場合には注意が必要です。

template<typename T, size_t N>
void print_array_size(std::array<T, N> arr) {
    std::cout << "Size inside function: " << arr.size() << std::endl;
}

3. サイズ情報を引数でそのまま渡す

そもそもサイズ情報を引数で渡してしまうという手もあります。一番シンプルですし一般的(?)です。

void print_array_size(int arr[], size_t size) {
    std::cout << "Size of array: " << size << std::endl;
}

4. std::spanを使用する(C++20以降)

C++20以降に追加されたstd::spanというクラスを使う方法もあるようですが、私の手元のコンパイラではバージョン非対応で検証できなかったため今回は説明割愛します。

参考文献

https://www.geeksforgeeks.org/what-is-array-decay-in-c-how-can-it-be-prevented/
https://web.archive.org/web/20081208122434/http://www.transcendentaxis.com/dthompson/blog/archives/9
http://rainbow.pc.uec.ac.jp/edu/program/b1/Ex4-1.htm
https://hensa40.cutegirl.jp/archives/2833

脚注
  1. 実際には警告が出力されますがここでは無視します ↩︎

Discussion