Объявление переменных внутри циклов, хорошей практики или плохой практики?

Вопрос № 1: Объявляет переменную внутри цикла хорошей практикой или плохой практикой?

Я прочитал другие темы о том, есть ли проблема с производительностью (большинство нет), и что вы всегда должны объявлять переменные как можно ближе к тому, где они будут использоваться. Мне интересно, следует ли это избегать или если это действительно предпочтительнее.

Пример:

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

   cout << testing;
}

Вопрос № 2:. Большинство компиляторов понимают, что переменная уже объявлена ​​и просто пропустить эту часть, или она фактически создает место для нее в памяти каждый раз?

Ответ 1

Это отличная практика.

Создавая переменные внутри циклов, вы гарантируете, что их область действия ограничена внутри цикла. Он не может ссылаться и не вызываться за пределами цикла.

Таким образом:

  • Если имя переменной является "общим" (например, "i" ), нет никакого риска смешать его с другой переменной с тем же именем где-то позже в вашем коде (также можно уменьшить с помощью -Wshadow предупреждение о GCC)

  • Компилятор знает, что область переменных ограничена внутри цикла, и поэтому выдаст правильное сообщение об ошибке, если эта ошибка по ошибке называется в другом месте

  • И последнее, но не менее важное: некоторая выделенная оптимизация может быть выполнена более эффективно компилятором (что наиболее важно для распределения регистров), поскольку известно, что переменная не может использоваться вне цикла. Например, нет необходимости хранить результат для последующего повторного использования.

Короче говоря, вы правы, чтобы это сделать.

Обратите внимание, однако, что переменная не должна сохранять свое значение между каждым циклом. В таком случае вам может понадобиться инициализировать его каждый раз. Вы также можете создать более крупный блок, охватывающий цикл, единственной целью которого является объявление переменных, которые должны сохранять свое значение из одного цикла в другой. Обычно это включает в себя счетчик циклов.

{
    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(). Вторая версия просто откажется работать, указав ясное сообщение об ошибке во время компиляции (лучше, чем время выполнения). В первой версии ничего не обнаружено, результат f1() будет просто протестирован во второй раз, запутанный для результата f2().

Дополнительная информация

Инструмент с открытым исходным кодом CppCheck (инструмент статического анализа для кода C/С++) дает некоторые отличные рекомендации относительно оптимальной области переменных.

В ответ на комментарий о распределении: Вышеприведенное правило верное в C, но может быть не для некоторых классов С++.

Для стандартных типов и структур размер переменной известен во время компиляции. В C нет такой вещи, как "конструкция", поэтому пространство для переменной просто будет выделено в стек (без какой-либо инициализации), когда вызывается функция. Вот почему существует "нулевая" стоимость при объявлении переменной внутри цикла.

Однако для классов С++ существует эта конструкторная вещь, о которой я знаю гораздо меньше. Я предполагаю, что распределение, вероятно, не будет проблемой, поскольку компилятор должен быть достаточно умным, чтобы повторно использовать одно и то же пространство, но инициализация, вероятно, будет иметь место на каждой итерации цикла.

Ответ 2

Как правило, очень хорошая практика держать его очень близким.

В некоторых случаях будет рассмотрен такой подход, как производительность, которая оправдывает выведение переменной из цикла.

В вашем примере программа создает и уничтожает строку каждый раз. В некоторых библиотеках используется небольшая оптимизация строк (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;
}

по-прежнему потребуется физическая копия символов на каждой итерации, но форма может привести к одному динамическому распределению, поскольку вы назначаете строку, а реализация должна видеть, что нет необходимости изменять размер выделения строки. Конечно, вы не сделали бы этого в этом примере (поскольку уже были продемонстрированы несколько превосходных альтернатив), но вы можете подумать об этом, когда меняется строка или векторный контент.

Итак, что вы делаете со всеми этими опциями (и более)? Держите его очень близко по умолчанию - пока вы не поймете затраты хорошо и не узнаете, когда вы должны отклоняться.

Ответ 3

Для С++ это зависит от того, что вы делаете. Хорошо, это глупый код, но представьте

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;

}

Вы будете ждать 55 секунд, пока не получите вывод myFunc. Просто потому, что каждый контур цикла и деструктор вместе нуждаются в 5 секундах для завершения.

Вам понадобится 5 секунд, пока вы не получите вывод myOtherFunc.

Конечно, это сумасшедший пример.

Но это иллюстрирует, что это может стать проблемой производительности, когда каждый цикл выполняет одну и ту же конструкцию, когда конструктору и/или деструктору требуется некоторое время.

Ответ 4

Я не отправлял сообщения, чтобы отвечать на вопросы ДжеремиРР (как они уже ответили); вместо этого я опубликовал просто, чтобы дать предложение.

В 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 С++ 2010 Express, поэтому я знаю, что он работает; Кроме того, я попытался использовать переменную за пределами скобок, в которой она была определена, и я получил ошибку, поэтому я знаю, что переменная была "уничтожена".

Я не знаю, использовать ли этот метод плохо, потому что многие немеченые скобки могут быстро сделать код нечитаемым, но, возможно, некоторые комментарии могут прояснить ситуацию.