Как сделать переменную цикла for постоянной, за исключением оператора приращения?

Рассмотрим стандартный цикл for:

for (int i = 0; i < 10; ++i) 
{
   // do something with i
}

Я хочу предотвратить изменение переменной i в теле цикла for.

Однако я не могу объявить i как const, так как это делает оператор приращения недействительным. Есть ли способ сделать i переменной const вне оператора приращения?


person jhourback    schedule 13.08.2020    source источник
comment
Я считаю, что нет никакого способа сделать это   -  person Itay    schedule 13.08.2020
comment
Вам нужно будет скрыть переменную из тела цикла, возможно, изменить что-то вроде while(i_copy = loop()) { }   -  person stark    schedule 13.08.2020
comment
Вы можете сделать постоянную ссылку на него в теле цикла, например. const int& i_safe = i. Ваш компилятор должен исключать любую косвенность.   -  person Brian    schedule 13.08.2020
comment
@Brian Это по-прежнему оставляет i уязвимым для тела вредоносного цикла.   -  person stark    schedule 13.08.2020
comment
Я предполагаю, что решение, которое избавится от i, не годится? Вы хотите иметь доступ только для чтения к i в цикле?   -  person cigien    schedule 13.08.2020
comment
Я всегда хотел предложить for({int i=0; i<10; ++i}){ для этого, но никогда не имел смелости предложить это комитету по стандартам или Бьярну, когда он добросовестно работает в Morgan Stanley. Конечно, мое предложение не позволит вам даже получить доступ к i в теле.   -  person Bathsheba    schedule 13.08.2020
comment
Если ты будешь молчать, ничего никогда не изменится. Если только он не собирается выпороть вас и вышвырнуть из университета за хорошее предложение (если да, то что вы там делаете?), то ничего не рискнул, ничего не выиграл.   -  person Michael Dorgan    schedule 13.08.2020
comment
Это звучит как решение в поисках проблемы.   -  person Pete Becker    schedule 13.08.2020
comment
Превратите тело вашего цикла for в функцию с аргументом const int i. Изменчивость индекса доступна только там, где это необходимо, и вы можете использовать ключевое слово inline, чтобы оно не влияло на скомпилированный вывод.   -  person Monty Thibault    schedule 14.08.2020
comment
Что (точнее, кто) может изменить значение индекса, кроме.... вас? Вы не доверяете себе? Может коллега? Я согласен с @PeteBecker.   -  person Z4-tier    schedule 14.08.2020
comment
@ Z4-tier Да, конечно, я себе не доверяю. Я знаю, что делаю ошибки. Каждый хороший программист знает. Вот почему у нас есть такие вещи, как const для начала.   -  person Konrad Rudolph    schedule 16.08.2020
comment
Если вы хотите избежать ошибок, вам вообще не следует использовать индексированные циклы for, так как они подвержены ошибкам off by one.   -  person Phil1970    schedule 17.08.2020
comment
На практике это никогда не должно быть проблемой. Если ваш цикл небольшой, то легко увидеть, что ì изменяется в цикле. Если цикл большой, то его следует преобразовать в функцию.   -  person Phil1970    schedule 17.08.2020


Ответы (9)


Начиная с С++ 20, вы можете использовать ranges::views::iota, например это:

for (int const i : std::views::iota(0, 10))
{
   std::cout << i << " ";  // ok
   i = 42;                 // error
}

Вот демонстрация.


Из С++ 11 вы также можете использовать следующую технику, в которой используется IIILE (немедленно вызываемое встроенное лямбда-выражение):

int x = 0;
for (int i = 0; i < 10; ++i) [&,i] {
    std::cout << i << " ";  // ok, i is readable
    i = 42;                 // error, i is captured by non-mutable copy
    x++;                    // ok, x is captured by mutable reference
}();     // IIILE

Вот демонстрация.

Обратите внимание, что [&,i] означает, что i захвачено неизменяемой копией, а все остальное захвачено изменяемой ссылкой. (); в конце цикла просто означает, что лямбда вызывается немедленно.

person cigien    schedule 13.08.2020
comment
Почти требует специальной конструкции цикла for, поскольку она предлагает более безопасную альтернативу очень и очень распространенной конструкции. - person Michael Dorgan; 13.08.2020
comment
@MichaelDorgan Что ж, теперь, когда эта функция поддерживается библиотекой, не стоит добавлять ее в качестве функции основного языка. - person cigien; 14.08.2020
comment
Справедливо, хотя почти вся моя реальная работа по-прежнему связана с C или C++11. Я учусь на всякий случай, если это будет иметь значение в будущем для меня... - person Michael Dorgan; 14.08.2020
comment
Трюк C++11, который вы добавили с лямбдой, хорош, но не был бы практичным в большинстве рабочих мест, в которых я работал. довольно громоздкий. Я также подозреваю, что это может привести к простым ошибкам, когда автор забывает (), из-за чего код никогда не вызывается. Это достаточно мало, чтобы его можно было пропустить при проверке кода. - person Human-Compiler; 14.08.2020
comment
это IIILE, если он ничего не инициализирует? конечно слишком много "я" - person Pete Kirkham; 14.08.2020
comment
Хотя технически лямбда является хорошим решением проблемы, на практике у вас больше шансов сделать в ней ошибку, чем маловероятная ошибка изменения i по ошибке. И код труднее понять большинству читателей. - person Phil1970; 17.08.2020
comment
@ Human-Compiler Я не уверен, что вижу проблемы, на которые вы указываете. clang по крайней мере предупредит, если вы пропустите (). Также предупреждение статического анализа кажется неправильным; тело цикла в коде OP должно иметь доступ к точно тем же самым переменным в результате захвата & в лямбде. - person cigien; 17.08.2020
comment
@Phil1970 Фил1970 Я понимаю проблему с тем, что большинство читателей не знают об этой идиоме, но какие именно ошибки вы имеете в виду? Можете ли вы показать некоторые потенциальные ловушки? - person cigien; 17.08.2020
comment
@cigien Инструменты статического анализа, такие как SonarQube, и cppcheck помечают общие захваты, как [&], потому что они конфликтуют с стандарты кодирования, такие как AUTOSAR (Правило A5-1-2), HIC++ и, я думаю, также MISRA (не уверен). Дело не в том, что это неправильно; это то, что организации запрещают этот тип кода, чтобы соответствовать стандартам. Что касается (), новейшая версия gcc не помечает это даже с -Wextra. Я все еще думаю, что подход опрятен; это просто не работает для многих организаций. - person Human-Compiler; 18.08.2020
comment
Я знаю, что правило SonarQube, которое я связал, гласит, что нет никакого вреда, если его использовать немедленно; однако на практике я видел, как он помечает любое использование [&] в разных средах. Возможно, это может быть связано с дополнительными правилами, установленными для соответствия стандартам кодирования, о которых я упоминал выше. Также тот факт, что gcc не предупреждает, когда не вызывается лямбда немедленно, я думаю, является одним из таких потенциальных падений, на которые, возможно, ссылался @Phil1970. - person Human-Compiler; 18.08.2020

Для тех, кому нравится ответ Cigien std::views::iota, но он не работает на C++20 или выше, довольно просто реализовать упрощенную и облегченную версию std::views::iota совместимого класса c++11 или выше.

Все, что для этого требуется, это:

  • Базовый тип LegacyInputIterator (что-то, что определяет operator++ и operator*), который упаковывает целочисленное значение. (например, int)
  • Некоторый класс, похожий на диапазон, который имеет begin() и end(), который возвращает вышеуказанные итераторы. Это позволит ему работать в циклах for на основе диапазона.

Упрощенная версия этого может быть:

#include <iterator>

// This is just a class that wraps an 'int' in an iterator abstraction
// Comparisons compare the underlying value, and 'operator++' just
// increments the underlying int
class counting_iterator
{
public:
    // basic iterator boilerplate
    using iterator_category = std::input_iterator_tag;
    using value_type = int;
    using reference  = int;
    using pointer    = int*;
    using difference_type = std::ptrdiff_t;

    // Constructor / assignment
    constexpr explicit counting_iterator(int x) : m_value{x}{}
    constexpr counting_iterator(const counting_iterator&) = default;
    constexpr counting_iterator& operator=(const counting_iterator&) = default;

    // "Dereference" (just returns the underlying value)
    constexpr reference operator*() const { return m_value; }
    constexpr pointer operator->() const { return &m_value; }

    // Advancing iterator (just increments the value)
    constexpr counting_iterator& operator++() {
        m_value++;
        return (*this);
    }
    constexpr counting_iterator operator++(int) {
        const auto copy = (*this);
        ++(*this);
        return copy;
    }

    // Comparison
    constexpr bool operator==(const counting_iterator& other) const noexcept {
        return m_value == other.m_value;
    }
    constexpr bool operator!=(const counting_iterator& other) const noexcept {
        return m_value != other.m_value;
    }
private:
    int m_value;
};

// Just a holder type that defines 'begin' and 'end' for
// range-based iteration. This holds the first and last element
// (start and end of the range)
// The begin iterator is made from the first value, and the
// end iterator is made from the second value.
struct iota_range
{
    int first;
    int last;
    constexpr counting_iterator begin() const { return counting_iterator{first}; }
    constexpr counting_iterator end() const { return counting_iterator{last}; }
};

// A simple helper function to return the range
// This function isn't strictly necessary, you could just construct
// the 'iota_range' directly
constexpr iota_range iota(int first, int last)
{
    return iota_range{first, last};
}

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

Приведенный выше шаблон позволяет следующему коду работать в версиях до C++20:

for (int const i : iota(0, 10))
{
   std::cout << i << " ";  // ok
   i = 42;                 // error
}

Который создаст такую ​​же сборку, что и решение C++20 std::views::iota, и классическое решение for-loop. при оптимизации.

Это работает с любыми компиляторами, совместимыми с C++11 (например, с такими компиляторами, как gcc-4.9.4), и по-прежнему создает почти идентичную сборку в базовый аналог цикла for.

Примечание. Вспомогательная функция iota предназначена только для обеспечения совместимости функций с решением C++20 std::views::iota; но на самом деле вы также можете напрямую построить iota_range{...} вместо вызова iota(...). Первый просто предлагает простой способ обновления, если пользователь захочет в будущем перейти на C++20.

person Human-Compiler    schedule 13.08.2020
comment
Это требует немного шаблонного кода, но на самом деле это не так уж сложно с точки зрения того, что он делает. На самом деле это просто базовый шаблон итератора, но он обертывает int, а затем создает класс диапазона для возврата начала/конца - person Human-Compiler; 14.08.2020
comment
Не очень важно, но я также добавил решение С++ 11, которое никто другой не публиковал, поэтому вы можете немного перефразировать первую строку своего ответа :) - person cigien; 14.08.2020
comment
Я не уверен, кто проголосовал против, но я был бы признателен за отзыв, если вы считаете, что мой ответ неудовлетворителен, чтобы я мог его улучшить. Голосование против - отличный способ показать, что вы чувствуете, что ответ неадекватно отвечает на вопрос, но в этом случае в ответе нет существующих критических замечаний или очевидных ошибок, которые я мог бы исправить. - person Human-Compiler; 14.08.2020
comment
@ Human-Compiler Я тоже получил DV в то же время, и они тоже не прокомментировали, почему :( Думаю, кому-то не нравятся абстракции диапазона. Я бы не беспокоился об этом. - person cigien; 14.08.2020
comment
сборка - это массовое существительное, как багаж или вода. Обычная формулировка будет такой: компилируется в ту же сборку, что и C++20.... Ассемблерный вывод компилятора для одной функции не является одной единственной сборкой, это сборка (последовательность инструкций на языке ассемблера). - person Peter Cordes; 14.08.2020
comment
@PeterCordes - Так же, как вода во множественном числе - это воды, множественное число от собрания - это собрания. Слово assembly, используемое здесь, обычно рассматривается как сокращенное от инструкции на языке ассемблера, и в этом случае, возможно, сборки лучше использовать во множественном числе. Но его также можно рассматривать как сокращение для набора (также известного как сборка) инструкций языка ассемблера, и в этом случае правильный термин действительно сборка. - person David Hammen; 15.08.2020
comment
@Human-Compiler — случайные отрицательные отзывы за хорошие ответы без каких-либо комментариев в отношении отрицательного ответа — обычное явление в сети StackExchange. - person David Hammen; 15.08.2020
comment
Моя единственная критика заключается в том, что функция iota конфликтует с шаблоном функции С++ 11 std::iota, который делает что-то совсем другое. Решение состоит в том, чтобы добавить конструктор в ваш класс iota_range. Хотя это противоречит c++20 std::iota_range, ваш iota_range и c++20 std::iota_range выполняют почти одно и то же. - person David Hammen; 15.08.2020
comment
@DavidHammen: Верно, в английском языке есть форма исчисляемого существительного слова «сборка», чтобы описать сборку вещей. В вычислительной технике у вас может быть сборка .NET; обратите внимание на неопределенный артикль a; у вас может быть несколько сборок .NET. Но в контексте вывода компилятора на языке ассемблера это массовое существительное, а не единственное число. Рассмотрим компилятор C++, который компилируется в C: если говорить о его выводе, можно сказать, что эти два источника C++ дают один и тот же C, а не один и тот же Cs. Язык ассемблера основан на неисчисляемой форме. - person Peter Cordes; 15.08.2020
comment
@PeterCordes В этом случае я использовал термин сборки, потому что имел в виду вывод кода сборки из двух разных источников; оптимизированный C++20 std::views::Iota и идиоматический цикл for. Я думаю, что в этом случае либо та же сборка, что и та же сборка, либо те же сборки, что и оба лексически правильны, хотя, перечитав это, я понимаю, почему это звучит неправильно. - person Human-Compiler; 16.08.2020
comment
@DavidHammen Я согласен с вашим комментарием: iota конфликтует, но std::views::iota также противоречит std::iota. Я в основном просто добавил это для паритета с эквивалентом С++ 20, но в остальном я полностью согласен с тем, что функция совершенно поверхностна и может быть выполнена только с диапазоном. Я решил, что проще представить решение с идентичными характеристиками установленному стандартному ответу, поскольку оно представляет собой более четкое решение для обновления, если проект принимает более новые стандарты. - person Human-Compiler; 16.08.2020

Версия ПОЦЕЛУЯ...

for (int _i = 0; _i < 10; ++_i) {
    const int i = _i;

    // use i here
}

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

person Artelius    schedule 14.08.2020
comment
Я думаю, вы преподаете неправильный урок, используя магические идентификаторы, начинающиеся с _. И немного пояснений (например, объем) было бы полезно. В противном случае, да, красиво KISSy. - person Yunnosch; 14.08.2020
comment
Вызов скрытой переменной i_ был бы более подходящим. - person Yirkha; 14.08.2020
comment
Я не уверен, как это отвечает на вопрос. Переменная цикла — _i, которую можно изменить в цикле. - person cigien; 14.08.2020
comment
@cigien: IMO, это частичное решение — это то, что стоит обойтись без C ++ 20 std::views::iota для полностью надежного способа. Текст ответа объясняет его ограничения и то, как он пытается ответить на вопрос. Куча чрезмерно сложного С++ 11 делает лекарство хуже, чем болезнь, с точки зрения легкости чтения и поддержки, IMO. Это по-прежнему очень легко читается для всех, кто знает C++, и кажется разумным как идиома. (Но следует избегать имен с подчеркиванием в начале.) - person Peter Cordes; 14.08.2020
comment
@Yunnosch зарезервированы только идентификаторы _Uppercase и double__underscore. Идентификаторы _lowercase зарезервированы только в глобальной области. - person Roman Odaisky; 14.08.2020

Не могли бы вы просто переместить часть или все содержимое вашего цикла for в функцию, которая принимает i как константу?

Это менее оптимально, чем некоторые предлагаемые решения, но, если возможно, это довольно просто сделать.

Изменить: просто пример, поскольку я склонен быть неясным.

for (int i = 0; i < 10; ++i) 
{
   looper( i );
}

void looper ( const int v )
{
    // do your thing here
}
person Al rl    schedule 15.08.2020

Если у вас нет доступа к c ++20, типичное преобразование с использованием функции

#include <vector>
#include <numeric> // std::iota

std::vector<int> makeRange(const int start, const int end) noexcept
{
   std::vector<int> vecRange(end - start);
   std::iota(vecRange.begin(), vecRange.end(), start);
   return vecRange;
}

теперь ты мог

for (const int i : makeRange(0, 10))
{
   std::cout << i << " ";  // ok
   //i = 100;              // error
}

(Посмотреть демо)


Обновление: Вдохновленный комментарием @Human-Compiler, мне было интересно, есть ли разница в данных ответах в случае производительности. Получается, что, кроме этого подхода, все остальные подходы на удивление имеют одинаковую производительность (для диапазона [0, 10)). Подход std::vector является худшим.

введите здесь описание изображения

(см. Quick-Bench в Интернете)

person JeJo    schedule 13.08.2020
comment
Хотя это работает до С++ 20, это имеет довольно большие накладные расходы, поскольку требует использования vector. Если диапазон очень большой, это может быть плохо. - person Human-Compiler; 13.08.2020
comment
@Human-Compiler: std::vector довольно ужасен в относительном масштабе, если диапазон также мал, и может быть очень плохо, если это должен был быть небольшой внутренний цикл, который выполнялся много раз. Некоторые компиляторы (например, clang с libc++, но не libstdc++) могут оптимизировать операцию new/delete выделения памяти, которая не выходит из функции, но в противном случае это легко может быть разницей между небольшим полностью развернутым циклом и вызовом new + delete, и, возможно, на самом деле сохранить в этой памяти. - person Peter Cordes; 14.08.2020
comment
IMO, незначительное преимущество const i просто не стоит накладных расходов в большинстве случаев, без способов С++ 20, которые делают его дешевым. Особенно с диапазонами переменных времени выполнения, которые снижают вероятность того, что компилятор все оптимизирует. - person Peter Cordes; 14.08.2020

А вот версия С++ 11:

for (int const i : {0,1,2,3,4,5,6,7,8,9,10})
{
    std::cout << i << " ";
    // i = 42; // error
}

Вот демонстрация в реальном времени

person Vlad Feinstein    schedule 13.08.2020
comment
Это не масштабируется, если максимальное число определяется значением времени выполнения. - person Human-Compiler; 13.08.2020
comment
@Human-Compiler Просто расширьте список до нужного значения и динамически перекомпилируйте всю программу;) - person Monty Thibault; 14.08.2020
comment
Вы не упомянули, как обстоят дела с {..}. Вам нужно включить что-то, чтобы активировать эту функцию. Например, ваш код сломается, если вы не добавите правильные заголовки: godbolt.org/z/esbhra . Ретрансляция <iostream> для других заголовков — плохая идея! - person JeJo; 14.08.2020

#include <cstdio>
  
#define protect(var) \
  auto &var ## _ref = var; \
  const auto &var = var ## _ref

int main()
{
  for (int i = 0; i < 10; ++i) 
  {
    {
      protect(i);
      // do something with i
      //
      printf("%d\n", i);
      i = 42; // error!! remove this and it compiles.
    }
  }
}

Примечание: нам необходимо вложить область действия из-за поразительной глупости языка: считается, что переменная, объявленная в заголовке for(...), находится на том же уровне вложенности, что и переменные, объявленные в составном операторе {...}. Это означает, что, например:

for (int i = ...)
{
  int i = 42; // error: i redeclared in same scope
}

Какая? Разве мы только что не открыли фигурную скобку? Более того, это несовместимо:

void fun(int i)
{
  int i = 42; // OK
}
person Kaz    schedule 14.08.2020
comment
Это легко лучший ответ. Элегантным решением является использование «затенения переменных» C++, чтобы заставить идентификатор разрешаться в переменную const ref, ссылающуюся на исходную переменную шага. Или, по крайней мере, самый элегантный из доступных. - person Max Barraclough; 15.08.2020

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

Например:

// A struct that holds the start and end value of the range
struct numeric_range
{
    int start;
    int end;

    // A simple function that wraps the 'for loop' and calls the function back
    template <typename Fn>
    void for_each(const Fn& fn) const {
        for (auto i = start; i < end; ++i) {
            const auto& const_i = i;
            fn(const_i);
        }
    }
};

Где будет использоваться:

numeric_range{0, 10}.for_each([](const auto& i){
   std::cout << i << " ";  // ok
   //i = 100;              // error
});

Все, что старше C++11, застрянет при передаче указателя функции со строгим именем в for_each (аналогично std::for_each), но все еще работает.

Вот демонстрация


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

Этот код также прост в написании, понимании и обслуживании.

person Human-Compiler    schedule 20.08.2020
comment
Одно ограничение, о котором следует помнить при таком подходе, заключается в том, что некоторые организации запрещают захват по умолчанию для лямбда-выражений (например, [&] или [=]) для соответствия определенным стандартам безопасности, что может привести к раздуванию лямбда-выражения, когда каждый элемент необходимо захватить вручную. Не все организации делают это, поэтому я упоминаю об этом только как комментарий, а не в ответе. - person Human-Compiler; 20.08.2020

template<class T = int, class F>
void while_less(T n, F f, T start = 0){
    for(; start < n; ++start)
        f(start);
}

int main()
{
    int s = 0;
    
    while_less(10, [&](auto i){
        s += i;
    });
    
    assert(s == 45);
}

можно назвать это for_i

Никаких накладных расходов https://godbolt.org/z/e7asGj

person Hrisip    schedule 09.11.2020
comment
Отсутствие аргумента лямбда auto const i действительно кажется неверным...! - person underscore_d; 06.02.2021
comment
@underscore_d смысл был в том, чтобы сделать переменную счетчика неизменяемой в цикле, что в моем примере равно start. Хотите вы i const или нет решать вам, так как это не суть, хоть и приятный бонус. - person Hrisip; 06.02.2021