Объявление переменных внутри циклов, хорошая практика или плохая практика? - PullRequest
207 голосов
/ 01 ноября 2011

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

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

Пример:

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

   cout << someString;
}

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

Ответы [ 4 ]

273 голосов
/ 01 ноября 2011

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

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

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

  • Если имя переменной немного «универсальное» (например, «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, но может не подходить для некоторых классов C ++.

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

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

17 голосов
/ 02 августа 2013

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

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

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

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

Выполнение этого:

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

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

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

13 голосов
/ 16 сентября 2015

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

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.

Конечно, это безумный пример.

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

8 голосов
/ 08 апреля 2015

Я не отвечал на вопросы 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.

Я не знаю, понимаете ли вы (я не знал, когда впервые начал программировать), что квадратные скобки (если они попарно) могут быть размещены в любом месте кода, а не только после «если», для "," пока "и т. д.

Мой код скомпилирован в Microsoft Visual C ++ 2010 Express, поэтому я знаю, что он работает; Кроме того, я попытался использовать переменную вне скобок, в которой она была определена, и я получил ошибку, поэтому я знаю, что переменная была «уничтожена».

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...