Ed S.は、ほとんどのコンパイラが両方の場合で同じコードを生成することを示しています。しかし、Maheshが指摘しているように、それらは実際には同一ではありません(i
バージョン1ではループスコープの外で使用することが合法であるがバージョン2では使用できないという明白な事実を超えても)。誤解を招かない方法で、これらの両方がどのように真実であるかを説明しようと思います。
まず、ストレージはi
どこから来るのですか?
標準はこれについては言及していません。ストレージがスコープの存続期間全体にわたって利用可能である限りi
、コンパイラが好きな場所であればどこでもかまいません。ただし、ローカル変数(技術的には、自動保存期間のある変数)を処理する一般的な方法は、適切なスコープのスタックフレームをsizeof(i)
バイト単位で拡張し、そのスタックフレームにi
オフセットとして保存することです。
「ティーチングコンパイラ」は、常にスコープごとにスタックフレームを作成する場合があります。しかし、実際のコンパイラは通常、特にループスコープの出入りで何も起こらない場合は気になりません。(アセンブリを確認するか、デバッガーに割り込む以外に違いを判断する方法はありません。もちろん、これを行うことは許可されています。)したがって、両方のバージョンは、おそらく、i
からのまったく同じオフセットを参照することになります。関数のスタックフレーム。(実際には、それi
はレジスターに入れられる可能性が非常に高いですが、それはここで重要なことを何も変更しません。)
それでは、ライフサイクルを見てみましょう。
最初のケースでは、コンパイラはi
関数スコープで宣言されている場所をデフォルトで初期化し、ループを通過するたびにコピーして割り当て、関数スコープの最後で破棄する必要があります。2番目のケースでは、コンパイラはi
各ループの開始時にコピー初期化を行い、各ループの終了時にそれを破棄する必要があります。このような:
i
クラスタイプの場合、これは非常に重要な違いになります。(理由が明らかでない場合は、以下を参照してください。)しかし、そうではありません。それはポインターです。これは、デフォルトの初期化と破棄が両方とも操作なしであり、コピーの初期化とコピーの割り当てが同じであることを意味します。
したがって、ライフサイクル管理コードはどちらの場合も同じになります。ループを通過するたびに1回コピーされ、それだけです。
言い換えると、ストレージは同じであることが許可されており、おそらく同じになるでしょう。ライフサイクル管理は同じである必要があります。
私がクラスタイプの場合、なぜこれらが異なるのかを説明することを約束しました。
この擬似コードを比較します。
i.IType();
for(j=0;j<10;j++) {
i.operator=(static_cast<IType>(getNthCon(j));
}
i.~IType();
これに:
for(j=0;j<10;j++) {
i.IType(static_cast<IType>(getNthCon(j));
i.~IType();
}
一見すると、最初のバージョンは1 IType()、10 operator =(IType&)、および1〜IType()であり、2番目のバージョンは10 IType(IType&)および10〜IType()であるため、「より良い」ように見えます。また、一部のクラスでは、これが当てはまる場合があります。しかし、operator =がどのように機能するかを考えると、通常、少なくともコピーの構築と破棄と同等のことを行う必要があります。
したがって、ここでの本当の違いは、最初のバージョンにはデフォルトのコンストラクターとコピー代入演算子が必要ですが、2番目のバージョンには必要ないということです。そして、そのstatic_castビットを取り出すと(つまり、コピーではなく変換コンストラクターと割り当てについて話している)、見ているものはこれと同等です。
for(j=0;j<10;j++) {
std::ifstream i(filenames[j]);
}
明らかに、その場合はループからiを引き出そうとします。
しかし、繰り返しますが、これは「ほとんどの」クラスにのみ当てはまります。バージョン2が途方もなく悪く、バージョン1の方が理にかなっているクラスを簡単に設計できます。