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

84

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

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

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

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

jhourback
источник
4
Я считаю, что это невозможно сделать
Итай,
27
Звучит как решение в поисках проблемы.
Пит Беккер,
14
Превратите тело цикла for в функцию с const int iаргументом. Изменчивость индекса отображается только там, где это необходимо, и вы можете использовать inlineключевое слово, чтобы оно не влияло на скомпилированный вывод.
Монти Тибо,
4
Что (точнее, кто) может изменить значение индекса, кроме ... вас? Вы не доверяете себе? Может, сослуживец? Я согласен с @PeteBecker.
Z4-tier
5
@ Z4-tier Да, конечно, я себе не доверяю. Я знаю, что делаю ошибки. Каждый хороший программист знает. Вот почему у нас есть такие вещи constдля начала.
Конрад Рудольф

Ответы:

120

Начиная с C ++ 20, вы можете использовать range :: views :: iota следующим образом:

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

Вот демо .


Начиная с C ++ 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это фиксируется неизменяемой копией, а все остальное фиксируется изменяемой ссылкой. Знак ();в конце цикла просто означает, что лямбда вызывается немедленно.

cigien
источник
Почти требует специальной конструкции цикла for, поскольку то, что она предлагает, является более безопасной альтернативой очень и очень распространенной конструкции.
Майкл Дорган,
2
@MichaelDorgan Что ж, теперь, когда есть поддержка библиотеки для этой функции, не стоит добавлять ее как функцию основного языка.
cigien
1
Честно, хотя почти вся моя настоящая работа по-прежнему C или C ++ 11, самое большее. Я учусь на всякий случай, если это будет иметь значение для меня в будущем ...
Майкл Дорган
9
Уловка C ++ 11, которую вы добавили с помощью лямбда, удобна, но не будет практичной на большинстве рабочих мест, в которых я был. Статический анализ будет жаловаться на обобщенный &захват, который заставит явно захватить каждую ссылку - что делает это довольно громоздкий. Я также подозреваю, что это может привести к легким ошибкам, когда автор забывает (), что делает код никогда не вызываемым. Это достаточно мало, чтобы не заметить его при проверке кода.
Human-Compiler
1
Инструменты статического анализа @cigien, такие как SonarQube и cppcheck, обычно фиксируют, [&]поскольку они конфликтуют со стандартами кодирования, такими как AUTOSAR (Правило A5-1-2), HIC ++ и, я думаю, также MISRA (не уверен). Это не значит, что это неправильно; Дело в том, что организации запрещают этот тип кода, чтобы он соответствовал стандартам. Что касается (), новейшая версия gcc не отмечает это даже с -Wextra. Я по-прежнему считаю подход изящным; это просто не работает для многих организаций.
Human-Compiler
44

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

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

  • Базовый тип 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аналогом -loop.

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

Человек-компилятор
источник
3
Для этого требуется немного шаблонов, но на самом деле это не так уж сложно с точки зрения того, что делает. На самом деле это просто базовый шаблон итератора, но обертывание int, а затем создание класса «диапазона» для возврата начала / конца
Human-Compiler
1
Не очень важно, но я также добавил решение на C ++ 11, которое никто не опубликовал, так что вы можете немного перефразировать первую строку своего ответа :)
cigien
Я не уверен, кто проголосовал против, но был бы признателен за обратную связь, если вы сочтете мой ответ неудовлетворительным, чтобы я мог его улучшить. Голосование против - отличный способ показать, что вы чувствуете, что ответ не дает адекватного ответа на вопрос, но в этом случае в ответе нет критических замечаний или очевидных ошибок, которые я мог бы улучшить.
Human-Compiler
@ Human-Compiler У меня в то же время появился DV, и они не объяснили, почему тоже :( Думаю, кому-то не нравятся абстракции диапазона. Я бы не стал об этом беспокоиться.
cigien
1
«сборка» - это такое же нарицательное, как «багаж» или «вода». Обычная фраза: «будет компилироваться в ту же сборку, что и C ++ 20 ...». Выход ASM компилятора для одной функции не сингулярная сборка, это «сборка» (последовательность команд на языке ассемблера).
Питер Кордес
29

Версия KISS ...

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

    // use i here
}

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

Artelius
источник
11
Я думаю, вы преподаете неверный урок, используя магические идентификаторы, начинающиеся с _. И небольшое пояснение (например, объем) было бы полезно. В остальном да, хорошо KISSy.
Yunnosch
14
Вызов «скрытой» переменной i_было бы более приемлемым.
Yirkha
9
Я не знаю, как это ответить на вопрос. Переменная цикла по- _iпрежнему может быть изменена в цикле.
cigien
4
@cigien: IMO, это частичное решение, насколько оно того стоит, без C ++ 20 std::views::iotaдля полностью пуленепробиваемого способа. Текст ответа объясняет его ограничения и то, как он пытается ответить на вопрос. Куча чрезмерно усложненного C ++ 11 делает лекарство хуже, чем болезнь, с точки зрения легкого для чтения и обслуживания IMO. Это все еще очень легко читать для всех, кто знает C ++, и кажется разумным в качестве идиомы. (Но следует избегать имен, начинающихся с подчеркивания.)
Питер Кордес,
5
Только @Yunnosch _Uppercaseи double__underscoreидентификаторы зарезервированы. _lowercaseидентификаторы зарезервированы только в глобальной области.
Роман Одайский
13

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

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

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

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

void looper ( const int v )
{
    // do your thing here
}
Al rl
источник
12

Если у вас нет доступа к , типичный макияж с использованием функции

#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Подход является наихудшим.

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

( См. Интерактивную быструю скамью )

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

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

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

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

Влад Файнштейн
источник
6
Это не масштабируется, если максимальное число определяется значением времени выполнения.
Human-Compiler
12
@ Human-Compiler Просто расширьте список до желаемого значения и динамически перекомпилируйте всю вашу программу;)
Монти Тибо,
5
Вы не упомянули, в чем дело {..}. Вам нужно что-то добавить, чтобы эта функция стала активной. Например, ваш код сломается, если вы не добавите правильные заголовки: godbolt.org/z/esbhra . Ретрансляция <iostream>для других заголовков - плохая идея!
JeJo
6
#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
}
Каз
источник
1
Это лучший ответ. Использование «затенения переменных» в C ++ для преобразования идентификатора в переменную const ref, ссылающуюся на исходную переменную шага, является элегантным решением. Или, по крайней мере, самый элегантный из имеющихся.
Макс Барраклаф
4

Еще не упомянутый здесь простой подход, который работает в любой версии 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 ++ , этот подход довольно распространен в других языках. Функциональные оболочки действительно изящны из-за их возможности компоновки сложных операторов и могут быть очень эргономичными для использования.

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

Человек-компилятор
источник
Одно ограничение, о котором следует помнить при таком подходе, заключается в том, что некоторые организации запрещают захват по умолчанию лямбда-выражений (например, [&]или [=]) в соответствии с определенными стандартами безопасности, что может привести к раздуванию лямбда-выражения, когда каждый член должен быть захвачен вручную. Не все организации делают это, поэтому я упоминаю это только как комментарий, а не как ответ.
Human-Compiler
0
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

Hrisip
источник