ループ内の変数を宣言する、良い習慣か悪い習慣か?


265

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

パフォーマンスの問題(ほとんどは「いいえ」と言われています)があるかどうかについて他のスレッドも読んだので、変数は使用する場所の近くに常に宣言する必要があります。これを回避する必要があるかどうか、またはそれが実際に望ましいかどうかは疑問です。

例:

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

   cout << someString;
}

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


29
プロファイリングで別段の指示がない限り、それらを使用方法に近づけます。
Mooing Duck、2011

1
:ここではいくつかの類似した質問ですstackoverflow.com/questions/982963/...の stackoverflow.com/questions/407255/...
drnewman

3
@drnewman私はそれらのスレッドを読みましたが、彼らは私の質問に答えませんでした。ループ内で変数を宣言すると機能することを理解しています。それをするのが良い習慣なのか、それとも避けるべき何かなのかと思います。
JeramyRR 2011年

回答:


348

これは素晴らしい練習です。

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

こちらです:

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

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

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

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

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

{
    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 { (...) }
    }

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

コンパイラーでさえもより効果的になります。将来、コードの誤った変更の後で、resultで適切に初期化されないと仮定しf2()ます。2番目のバージョンは単に動作を拒否し、コンパイル時に明確なエラーメッセージを示します(実行時よりも優れています)。最初のバージョンは何も見つけられず、の結果f1()は単純に2回テストされ、の結果と混同されf2()ます。

補足情報

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

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

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

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


4
素晴らしい答え。これはまさに私が探していたものであり、気づかなかった何かへの洞察さえも与えてくれました。スコープがループ内のみに留まることを知りませんでした。ご回答ありがとうございます!
JeramyRR、2011年

22
「しかし、関数の最初に割り当てるよりも遅くなることは決してありません。」これは常に正しいとは限りません。変数は1回割り当てられますが、必要に応じて何度でも構築および破棄されます。コード例の場合、これは11倍です。Mooingのコメントを引用するには、「プロファイリングで別段の指示がない限り、使用方法に近づけてください。」
IronMensan、2011年

4
@JeramyRR:絶対にありません-コンパイラは、オブジェクトのコンストラクタまたはデストラクタに意味のある副作用があるかどうかを知る方法がありません。
ildjarn、2011年

2
@Iron:一方、アイテムを最初に宣言すると、代入演算子が何度も呼び出されます。通常、オブジェクトの構築と破棄とほぼ同じコストがかかります。
ビリーONeal 2013

4
@BillyONeal:の場合stringvector、代入演算子は、(あなたのループに応じて)各ループ、バッファ割り当て再利用することができ、具体的に膨大な時間を節約することができます。
Mooing Duck

22

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

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

あなたの例では、プログラムは毎回文字列を作成および破棄します。一部のライブラリは小さな文字列最適化(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つの動的割り当てになる可能性があります。もちろん、この例ではそれはしません(複数の優れた代替案がすでに示されているため)が、文字列またはベクトルの内容が異なる場合は、それを検討することもできます。

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


1
floatやintのような基本的なデータ型に関しては、ループの内側で変数を宣言する方が、ループの外側で変数を宣言するよりも遅くなります。繰り返しごとに変数にスペースを割り当てる必要があるからです。
Kasparov92

2
@ Kasparov92短い答えは「いいえ。可読性/局所性を改善するために、その最適化を無視してループに配置します。可能な場合はコンパイラーがマイクロ最適化を実行します。」より詳細には、プラットフォーム、最適化レベルなどに最適なものに基づいて、最終的にコンパイラが決定します。ループ内の通常のint / floatは通常、スタックに配置されます。コンパイラーは、最適化が行われていれば、それをループの外に移動してストレージを再利用できます。実用的な目的のために、これは非常に非常に非常に小さい最適化...だろう
ジャスティン

1
@ Kasparov92…(続き)すべてのサイクルがカウントされる環境/アプリケーションでのみ考慮します。その場合は、アセンブリの使用を検討してください。
ジャスティン2018年

14

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秒必要です。

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

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


2
あなたはまだ、オブジェクトを破壊していないので、まあ、技術的には第二のバージョンであなたは.....、わずか2秒で出力を取得します
クリス

12

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」、「ながら」など

私のコードはMicrosoft Visual C ++ 2010 Expressでコンパイルされているので、機能することはわかっています。また、変数が定義されている括弧の外で変数を使用しようとしたところ、エラーが発生したため、変数が「破棄された」ことがわかります。

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


4
私にとって、これは質問に直接リンクされた提案をもたらす非常に正当な答えです。私の投票があります!
Alexis Leclerc

0

上記のすべての回答が質問の非常に優れた理論的側面を提供しているので、これは非常に良い習慣です。コードを垣間見させてください。GEEKSFORGEEKSでDFSを解決しようとしていましたが、最適化の問題に遭遇しました......ループ外で整数を宣言するコードを解くと、最適化エラーが発生します。

stack<int> st;
st.push(s);
cout<<s<<" ";
vis[s]=1;
int flag=0;
int top=0;
while(!st.empty()){
    top = st.top();
    for(int i=0;i<g[top].size();i++){
        if(vis[g[top][i]] != 1){
            st.push(g[top][i]);
            cout<<g[top][i]<<" ";
            vis[g[top][i]]=1;
            flag=1;
            break;
        }
    }
    if(!flag){
        st.pop();
    }
}

整数をループ内に配置すると、正しい答えが得られます...

stack<int> st;
st.push(s);
cout<<s<<" ";
vis[s]=1;
// int flag=0;
// int top=0;
while(!st.empty()){
    int top = st.top();
    int flag = 0;
    for(int i=0;i<g[top].size();i++){
        if(vis[g[top][i]] != 1){
            st.push(g[top][i]);
            cout<<g[top][i]<<" ";
            vis[g[top][i]]=1;
            flag=1;
            break;
        }
    }
    if(!flag){
        st.pop();
    }
}

これは、2番目のコメントで@justin先生が言っていた内容を完全に反映してい ます。ちょうどそれを打つ....あなたはそれを得るでしょう。この助けを願っています


これは質問には当てはまらないと思います。明らかに、あなたの上記のケースではそれが重要です。問題は、コードの動作を変更せずに変数定義を他の場所で定義できる場合に対処することでした。
1

あなたが投稿したコードでは、問題は定義ではなく初期化の部分です。反復flagごとに0で再初期化する必要がありwhileます。これは論理の問題であり、定義の問題ではありません。
MartinVéronneau

0

第4.8章K&Rのブロック構造C言語プログラミング言語2。

ブロックで宣言および初期化された自動変数は、ブロックに入るたびに初期化されます。

次のような本の関連する説明を見逃したかもしれません。

ブロックで宣言および初期化された自動変数は、ブロックに入る前に一度だけ割り当てられます。

しかし、簡単なテストは、仮定が保持されていることを証明できます。

 #include <stdio.h>                                                                                                    

 int main(int argc, char *argv[]) {                                                                                    
     for (int i = 0; i < 2; i++) {                                                                                     
         for (int j = 0; j < 2; j++) {                                                                                 
             int k;                                                                                                    
             printf("%p\n", &k);                                                                                       
         }                                                                                                             
     }                                                                                                                 
     return 0;                                                                                                         
 }                                                                                                                     
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.