C++において、関数の引数として配列を渡す際に重要なポイントがあります。特に、const char str[N]を使用すると予期しない問題が発生することがあります。この記事では、その問題と解決策について詳しく説明します。
問題点: 配列の退化
まず、const char str[N]という形で関数に配列を渡すと、コンパイラはこれをconst char*に変換してしまいます。この現象を「配列の退化」と呼びます。これにより、配列の長さ情報が失われ、テンプレートパラメータであるNも推論できなくなります。
正しい方法: 配列参照を使う
配列の長さ情報を保持し、型安全性を確保するためには、const char (&str)[N]という形式を使用する必要があります。この書き方により、配列の長さ情報が保持され、テンプレートパラメータの推論も可能になります。
背景: 配列退化の仕組み
C++の規格では、配列が関数の引数や代入文に現れる場合、暗黙的にその先頭要素へのポインタに変換されます。このルールはC言語から引き継がれており、以下の場合を除いて適用されます:
- アドレス取得演算子(&)
- 配列添字演算子([])
sizeof演算子typeid演算子
このため、const char str[N]のように書いた場合、実際にはconst char* strと同じ意味になり、Nは無視されます。
解決策: 強い型チェックによる退化防止
参照(&)を使うことで、この退化を防ぐことができます。参照は既存オブジェクトの別名であり、型チェックが厳密に行われるため、配列の完全な型(要素の型と長さ)を保持できます。
例: 退化を確認するコード
#include <iostream>
using namespace std;
// 配列が退化する例
void test(const char str[100]) {
cout << typeid(decltype(str)).name() << endl; // 出力: const char*
}
int main() {
const char arr[5] = "test";
test(arr); // 配列arrがconst char*に退化
return 0;
}
例: 参照で長さを保持するコード
#include <iostream>
using namespace std;
// 配列参照を使用する例
template<size_t N>
void test(const char (&str)[N]) {
cout << typeid(decltype(str)).name() << endl; // 出力: const char [6]
cout << "N = " << N << endl; // 出力: 6
}
int main() {
test("hello"); // const char[6]として認識される
return 0;
}
補足: 退化の設計意図
配列の退化はC言語の歴史的なデザインです。初期のC言語では参照が存在しなかったため、配列全体をコピーする必要があり、これは効率が悪かったです。そのため、「配列をポインタに退化させる」ルールが導入されました。しかし、これにより長さ情報が失われる欠点があります。C++では、この問題を解決するために「配列参照」が提供されています。