web-dev-qa-db-ja.com

ループ内で変数を宣言しますか?

質問#1:ループ内で変数を宣言するのは良い習慣ですか、それとも悪い習慣ですか?

パフォーマンスの問題があるかどうかについて(他のスレッドはほとんど言っていませんが)、他のスレッドを読んで、変数を使用する場所の近くで常に宣言する必要があります。私が疑問に思っているのは、これを避けるべきかどうか、それが実際に好まれているかどうかです。

例:

for(int counter = 0; counter <= 10; counter++)
{
   string someString = "testing";

   cout << someString;
}

質問#2:ほとんどのコンパイラーは、変数がすでに宣言されていることを認識し、その部分をスキップするだけですか、それとも実際に毎回メモリ内にその場所を作成しますか?

204
JeramyRR

これはexcellentプラクティスです。

ループ内に変数を作成することにより、そのスコープがループ内に制限されるようにします。ループ外で参照したり呼び出したりすることはできません。

こちらです:

  • 変数の名前が少し「ジェネリック」(「i」など)の場合、コードのどこかで同じ名前の別の変数と混合するリスクはありません(GCCで-Wshadow警告命令を使用して軽減することもできます)

  • コンパイラは、変数のスコープがループ内に制限されていることを認識しているため、変数が他の場所で誤って参照された場合、適切なエラーメッセージを発行します。

  • 最後に大事なことを言い忘れていましたが、コンパイラーは変数をループ外で使用できないことを知っているため、コンパイラーによって専用の最適化をより効率的に実行できます(最も重要なのはレジスター割り当て)。たとえば、後で再利用するために結果を保存する必要はありません。

要するに、あなたはそれを行う権利があります。

ただし、変数は各ループ間で値を保持することを想定していないことに注意してください。そのような場合、毎回初期化する必要があります。ループを取り囲む大きなブロックを作成することもできます。その唯一の目的は、ループ間で値を保持する必要がある変数を宣言することです。これには通常、ループカウンター自体が含まれます。

{
    int i, retainValue;
    for (i=0; i<N; i++)
    {
       int tmpValue;
       /* tmpValue is uninitialized */
       /* retainValue still has its previous value from previous loop */

       /* Do some stuff here */
    }
    /* Here, retainValue is still valid; tmpValue no longer */
}

質問#2の場合:変数は、関数が呼び出されたときに一度割り当てられます。実際、割り当ての観点から見ると、これは関数の先頭で変数を宣言するのと(ほぼ)同じです。唯一の違いはスコープです。変数はループ外で使用できません。変数が割り当てられておらず、(スコープが終了した他の変数からの)空きスロットを再利用することも可能です。

制限されたより正確なスコープにより、より正確な最適化が行われます。しかし、さらに重要なことは、コードの安全性を高め、コードの他の部分を読み取るときに心配する必要のない状態(変数など)を減らすことです。

これは、if(){...}ブロックの外側でも同様です。通常、代わりに:

    int result;
    (...)
    result = f1();
    if (result) then { (...) }
    (...)
    result = f2();
    if (result) then { (...) }

書く方が安全です:

    (...)
    {
        int const result = f1();
        if (result) then { (...) }
    }
    (...)
    {
        int const result = f2();
        if (result) then { (...) }
    }

特にこのような小さな例では、違いはわずかに見えるかもしれません。しかし、より大きなコードベースでは、それは助けになります:f1()からf2()ブロックにresult値を転送するリスクはありません。各resultは、それ自体のスコープに厳密に制限されており、その役割をより正確にします。レビューアの観点から見ると、心配して追跡する必要があるロングレンジ状態変数が少ないため、はるかに優れています。

コンパイラでさえもより良くなります。将来、コードの何らかの誤った変更の後、resultf2()で適切に初期化されないと仮定します。 2番目のバージョンは、単に実行を拒否し、コンパイル時に明確なエラーメッセージを表示します(実行時よりも良い方法です)。最初のバージョンでは何も検出されず、f1()の結果は、f2()の結果と混同されて、2回目のテストが行​​われます。

補足情報

オープンソースツール CppCheck (C/C++コードの静的分析ツール)は、変数の最適な範囲に関する優れたヒントを提供します。

割り当てに関するコメントへの回答:上記の規則はCでは正しいですが、一部のC++クラスには当てはまらない場合があります。

標準のタイプと構造の場合、変数のサイズはコンパイル時にわかります。 Cには「構築」というものはありません。そのため、関数が呼び出されると、変数のスペースが(初期化なしで)単にスタックに割り当てられます。これが、ループ内で変数を宣言するときに「ゼロ」コストがある理由です。

しかし、C++クラスの場合、このコンストラクターについてはあまり知りません。コンパイラーは同じスペースを再利用するのに十分賢いはずなので、おそらく割り当ては問題にならないでしょうが、初期化はループの繰り返しごとに行われる可能性が高いです。

271
Cyan

一般的に、非常に近くに保つことは非常に良い習慣です。

場合によっては、変数をループから引き出すことを正当化するパフォーマンスなどの考慮事項があります。

例では、プログラムは毎回文字列を作成および破棄します。一部のライブラリは、小さな文字列最適化(SSO)を使用するため、場合によっては動的割り当てを回避できます。

これらの冗長な作成/割り当てを避けたい場合、次のように記述します。

for (int counter = 0; counter <= 10; counter++) {
   // compiler can pull this out
   const char testing[] = "testing";
   cout << testing;
}

または、定数を引き出すことができます:

const std::string testing = "testing";
for (int counter = 0; counter <= 10; counter++) {
   cout << testing;
}

ほとんどのコンパイラは、変数が既に宣言されていることを認識して、その部分をスキップするだけですか、それとも実際に毎回メモリ内に変数のスポットを作成しますか?

変数が消費するスペースを再利用でき、不変式をループから引き出すことができます。 const char配列の場合(上記)-その配列を引き出すことができます。ただし、コンストラクターとデストラクターは、オブジェクト(std::stringなど)の場合、反復ごとに実行する必要があります。 std::stringの場合、その「スペース」には、文字を表す動的割り当てを含むポインターが含まれます。したがって、この:

for (int counter = 0; counter <= 10; counter++) {
   string testing = "testing";
   cout << testing;
}

いずれの場合も冗長コピーが必要であり、変数がSSO文字数のしきい値を超えている場合(およびSSOはstdライブラリによって実装されている場合)、動的割り当てと解放が必要になります。

これを行う:

string testing;
for (int counter = 0; counter <= 10; counter++) {
   testing = "testing";
   cout << testing;
}

繰り返しごとに文字の物理コピーが必要になりますが、文字列を割り当てると、実装によって文字列のバッキング割り当てのサイズを変更する必要がないため、フォームは1つの動的割り当てになります。もちろん、この例ではこれを行いません(複数の優れた代替手段が既に実証されているため)が、文字列またはベクターのコンテンツが異なる場合は考慮してもかまいません。

では、これらすべてのオプション(およびそれ以上)で何をしますか?コストを十分に理解し、いつ逸脱すべきかを知るまで、デフォルトとして非常に近くしてください。

17
justin

C++の場合は、実行内容によって異なります。 OK、それは愚かなコードですが、想像してみてください

class myTimeEatingClass
{
 public:
 //constructor
      myTimeEatingClass()
      {
          sleep(2000);
          ms_usedTime+=2;
      }
      ~myTimeEatingClass()
      {
          sleep(3000);
          ms_usedTime+=3;
      }
      const unsigned int getTime() const
      {
          return  ms_usedTime;
      }
      static unsigned int ms_usedTime;
};
myTimeEatingClass::ms_CreationTime=0; 
myFunc()
{
    for (int counter = 0; counter <= 10; counter++) {

        myTimeEatingClass timeEater();
        //do something
    }
    cout << "Creating class took "<< timeEater.getTime() <<"seconds at all<<endl;

}
myOtherFunc()
{
    myTimeEatingClass timeEater();
    for (int counter = 0; counter <= 10; counter++) {
        //do something
    }
    cout << "Creating class took "<< timeEater.getTime() <<"seconds at all<<endl;

}

MyFuncの出力を取得するまで55秒待機します。各ループコンストラクターとデストラクタが一緒に終了するのに5秒かかるためです。

MyOtherFuncの出力を取得するまで5秒かかります。

もちろん、これはおかしな例です。

しかし、コンストラクターやデストラクタがある程度の時間を必要とするときに、各ループで同じ構成が行われると、パフォーマンスの問題になる可能性があることを示しています。

12
Nobby

JeremyRRの質問に回答するために投稿しませんでした(既に回答済みです)。代わりに、単に提案をするために投稿しました。

JeremyRRには、これを行うことができます。

{
  string someString = "testing";   

  for(int counter = 0; counter <= 10; counter++)
  {
    cout << someString;
  }

  // The variable is in scope.
}

// The variable is no longer in scope.

あなたが知っているかどうかはわかりません(最初にプログラミングを始めたときは知りませんでした)。ブラケットは(ペアになっている限り)コード内のどこにでも配置できることを「if」、「for」、「 while」など.

私のコードはMicrosoft Visual C++ 2010 Expressでコンパイルされているため、動作することはわかっています。また、定義されている括弧の外側の変数を使用しようとしましたが、エラーを受け取りました。そのため、変数が「破壊された」ことがわかります。

ラベルのない括弧がたくさんあるとコードがすぐに読めなくなる可能性があるため、このメソッドを使用するのが悪い習慣であるかどうかはわかりませんが、いくつかのコメントで問題を解決できるかもしれません。

8
Fearnbuster