Як зробити змінну const змінної con, за винятком оператора збільшення?


84

Розглянемо стандарт для циклу:

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

Я хочу запобігти зміні змінної iв тілі forциклу.

Однак я не можу оголосити i, constоскільки це робить оператор збільшення недійсним. Чи є спосіб , щоб зробити iз constперемінним поза заяви збільшення?


4
Я вважаю, що це неможливо зробити
Ітай

27
Це звучить як рішення у пошуках проблеми.
Піт Беккер

14
Перетворіть тіло вашого циклу for у функцію з const int iаргументом. Змінюваність індексу піддається лише там, де це потрібно, і ви можете використовувати inlineключове слово, щоб воно не впливало на скомпільований результат.
Монті Тібо

4
Що (вірніше, хто) може змінити значення індексу, крім .... вас? Ви не довіряєте собі? Може, колега по роботі? Я згоден з @PeteBecker.
Z4 рівень

5
@ Z4-рівень Так, звичайно, я не довіряю собі. Я знаю, що роблю помилки. Кожен хороший програміст знає. Ось чому у нас є речі, як constдля початку.
Конрад Рудольф

Відповіді:


120

З c ++ 20 ви можете використовувати діапазони :: 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фіксується незмінною копією, а все інше фіксується змінною посиланням. ();В кінці циклу просто означає , що лямбда викликається негайно.


Майже вимагає спеціальної конструкції для циклу, оскільки те, що вона пропонує, є більш безпечною альтернативою дуже, дуже поширеній конструкції.
Michael Dorgan

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

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

Для цього потрібно лише:

  • Базовий тип " LegacyInputIterator " (щось, що визначає operator++і operator*), що обертає інтегральне значення (наприклад, an 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 у майбутньому.


3
Це вимагає трохи зразка, але насправді це не все так складно з точки зору того, що робиться. Це насправді просто базовий шаблон ітератора, але обгортання intкласу "діапазон" для повернення початку / кінця
Human-Compiler

1
Не надто важливо, але я також додав рішення c ++ 11, яке ніхто не публікував, тож ви можете трохи
переформулювати

Я не впевнений, хто голосував за, але я був би вдячний за відгуки, якщо ви вважаєте, що моя відповідь незадовільна, щоб я міг її покращити. Подалене голосування - чудовий спосіб показати, що ви вважаєте, що відповідь не вирішує питання належним чином, але в цьому випадку у відповіді немає жодної критики чи очевидних помилок, щодо яких я можу покращитись.
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
}

Якщо ви використовуєте лише запобігання випадковій зміні індексу циклу, це повинно зробити таку помилку очевидною. (Якщо ви хочете запобігти навмисному внесенню змін, ну, удачі ...)


11
Думаю, ви викладаєте неправильний урок, використовуючи магічні ідентифікатори, які починаються з _. І трохи пояснень (наприклад, сфера застосування) було б корисним. Інакше так, приємно ПОЦІЛУЙ.
Юннош

14
Виклик змінної "прихований" i_був би більш сумісним.
Yirkha

9
Я не впевнений, як це відповідає на запитання. Змінна циклу - це те, _iщо все ще можна змінювати в циклі.
cigien

4
@cigien: ІМО, це часткове рішення настільки, наскільки варто обійтися без C ++ 20 std::views::iotaповністю куленепробивальним способом. Текст відповіді пояснює його обмеження та спосіб спроби відповісти на запитання. Купа надскладного C ++ 11 робить лікування гіршим, ніж хвороба, з точки зору легко читається, легко підтримується, ІМО. Це все ще дуже легко прочитати для всіх, хто знає C ++, і здається розумним як ідіома. (Але слід уникати імен, що підкреслюють підкреслення.)
Пітер Кордес,

5
Лише @Yunnosch _Uppercaseта double__underscoreідентифікатори зарезервовані. _lowercaseідентифікатори зарезервовані лише у глобальній області.
Роман Одайський

13

Чи не могли б ви просто перемістити частину або весь вміст циклу for у функцію, яка приймає i як const?

Він менш оптимальний, ніж пропоновані рішення, але, якщо це можливо, це зробити досить просто.

Редагувати: Це лише приклад, оскільки я, як правило, незрозумілий.

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

void looper ( const int v )
{
    // do your thing here
}

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Підхід є найгіршим.

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

( Див. Інтернет-швидкий тест )


4
Хоча це працює для pre-c ++ 20, це має досить велику кількість накладних витрат, оскільки вимагає використання vector. Якщо діапазон дуже великий, це може бути погано.
Human-Compiler

@ Human-Compiler: A std::vectorє досить жахливим у відносному масштабі, якщо діапазон також невеликий, і може бути дуже поганим, якщо це повинен був бути маленький внутрішній цикл, який запускався багато разів. Деякі компілятори (наприклад, clang з libc ++, але не libstdc ++) можуть оптимізувати новий / видалений розподіл, який не уникне функції, але в іншому випадку це може бути різниця між невеликим повністю розгорнутим циклом та викликом new+ delete, і, можливо, насправді зберігається в цій пам’яті.
Пітер Кордес,

ІМО, незначна вигода від const iцього просто не варта накладних витрат у більшості випадків, без C ++ 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, що посилається на вихідну змінну кроку. Або, принаймні, найелегантніший із наявних.
Max Barraclough

4

Одним простим підходом, про який ще не згадувалося тут, який працює в будь-якій версії С ++, є створення функціональної обгортки навколо діапазону, подібного до того, що 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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.