C++11 range forは多次元配列を遍歴し、反復変数は「&」を参照する問題を追加する

3019 ワード

問題の背景
autoはC++11拡張の新しい特性であり,自動タイプ導出に用いられる.ただし、導出したタイプが参照(&)である場合は、変数名に手動で'&'を付けます.変数が定数である必要がある場合は、「const」修飾子も手動で追加します.
長話では、本編の博文の目的は、ranger forを用いて多次元配列を遍歴する際、変数名に引用を加えた'&'の問題を述べ、それを説明することであり、本博文の参考文献[1]『C++Primer 5 th』には言及されているが、紙面は小さいため、ここでは詳細に構想を整理する.
 
問題を引き起こす.
次に,autoとrange for(C++11拡張の遍歴特性)を用いて2次元配列を遍歴して問題を引き出す.(range for、スタイルはfor(xxxx:yyyy)であり、forループの形式であり、本稿ではその使い方を知っていると仮定する)
コードは次のようになります.
#include 
#include 
using namespace std;

int main() {
	constexpr size_t rowCnt = 3, colCnt = 4;
	int arr[rowCnt][colCnt] = {};	//initialized to all zero

	//method1 : successful compilation  and iteration
	for(auto &row : arr)
		for(auto col : row)
			cout << col << endl;

	/*
	//method2: compile error
	for(auto row : arr)
		for(auto col : row)
			cout << col << endl;
			*/
	return 0;
}

その中で、方式は成功してコンパイルすることができて、しかも正しく運行することができます;方式2はコンパイルされていません.なぜですか.この2つの方法の内部メカニズムを説明する.
 
説明する
方式一
まず外層ループ文を見て、for(auto&row:arr)という文は、参照rowを定義し、リフレッシュ(1サイクル後、参照はライフサイクルを過ぎ、再定義が必要)反復変数rowを定義するたびに、rowはarrの長さ4の配列をバインドし、int(&row)[4]=arr[i](i=0,1,2)に等価である.
次に、rowは、内層サイクルの場合、長さ4の1次元配列の参照である.
内層サイクル:for(auto col:row)、rowを参照してint[]の1次元配列をバインドし、1次元配列を遍歴するのと同じように問題ありません.
方式2
まず外層循環文を見ると、for(auto row:arr)、arrの本来のタイプはint[][]であり、参考文献[1]によると、rowが参照と定義されていない場合、arrは自動的にint**タイプに変換され、rowは中の要素としてint*であり、その値はrowに対応するarrの1つの要素(長さ4の配列)の最初の要素のポインタです.
では,内層サイクル:for(auto col:row)では,colはint[]ではなくint*を遍歴する.一方,方式1の内層サイクルは遍歴int[]である.
これにより,誤りの根源は「type*を巡る誤り,type[]を巡ることができる」ことにある可能性が高いことが分かった.
この問題を検証する例を書きます.
int list[5] = {1,2,3,4,5};

//     int []     int *,     p 
int *p = list;

//compile error, attempting to iterate int *
for(auto e : p)
	cout << e << " ";

//correct, attempting to iterate int []
for(auto e : list)
	cout << e << " ";

ここまで、私たちの問題は、コンパイラが自動的に[]を*に変換しないようにすることに相当することを理解しました.配列タイプ[]では、コンパイラは境界を決定してアクセスできますが、ポインタタイプ*では、コンパイラ自身がどのように遍歴するかを決定することはできません.これはrange for---for(xxx:yy)形式の不足です.通常forでは、このような問題は存在しない.通常forのポインタ遍歴では、ポインタに開始位置を初期化し、境界位置を指定し、ポインタの移動方法(++、--、+=、-=...)も選択できるからだ.そこで、本編のブログは、ranger forをめぐって議論されています. 
decltypeの使用
もちろんautoで自動タイプの導出をしたくない場合はdecltypeを使用します.上の例ではdecltypeでは確かに混同されにくいので、下のコードを見てみると、簡単で直接的ではないでしょうか.
	for(decltype(arr[0]) &row : arr)
		for(decltype(arr[0][0]) col : row)
			cout << col << endl;
       
もちろん、「&」を取り除くのも問題ありません.decltypeのタイプはarr[0]とarr[0][0]から導出されています.これはint[]とint[][]であり、コンパイラはそれらをint*とint**に変換しません.
	for(decltype(arr[0]) row : arr)
		for(decltype(arr[0][0]) col : row)
			cout << col << endl;

次元の拡張
range forの方法で、より高い次元の配列(dim>2)を遍歴したい場合は、最内層サイクルを除いて、他のすべての外層サイクルに'&'を加えるだけです.
References
[1] 《C++ Primer Fifth Edition》  Stanley B. Lippman, Josée Lajoie, Barbara E. Moo