How to make a for loop variable const with the exception of the increment statement?
From c++20, you can use ranges::views::iota like this:
for (int const i : std::views::iota(0, 10))
{
std::cout << i << " "; // ok
i = 42; // error
}
Here's a demo.
From c++11, you can also use the following technique, which uses an IIILE (immediately invoked inline lambda expression):
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
Here's a demo.
Note that [&,i]
means that i
is captured by non-mutable copy, and everything else is captured by mutable reference. The ();
at the end of the loop simply means that the lambda is invoked immediately.
For anyone that likes Cigien's std::views::iota
answer but isn't working in C++20 or above, it's rather straightforward to implement a simplified and lightweight version of std::views::iota
compatible c++11 or above.
All it requires is:
- A basic "LegacyInputIterator" type (something that defines
operator++
andoperator*
) that wraps an integral value (e.g. anint
) - Some "range"-like class that has
begin()
andend()
that returns the above iterators. This will allow it to work in range-basedfor
loops
A simplified version of this could be:
#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};
}
I've defined the above with constexpr
where it's supported, but for earlier versions of C++ like C++11/14, you may need to remove constexpr
where it is not legal in those versions to do so.
The above boilerplate enables the following code to work in pre-C++20:
for (int const i : iota(0, 10))
{
std::cout << i << " "; // ok
i = 42; // error
}
Which will generate the same assembly as the C++20 std::views::iota
solution and the classic for
-loop solution when optimized.
This works with any C++11-compliant compilers (e.g. compilers like gcc-4.9.4
) and still produces nearly identical assembly to a basic for
-loop counterpart.
Note: The iota
helper function is just for feature-parity with the C++20 std::views::iota
solution; but realistically, you could also directly construct an iota_range{...}
instead of calling iota(...)
. The former just presents an easy upgrade path if a user wishes to switch to C++20 in the future.
The KISS version...
for (int _i = 0; _i < 10; ++_i) {
const int i = _i;
// use i here
}
If your use case is just to prevent accidental modification of the loop index then this should make such a bug obvious. (If you want to prevent intentional modification, well, good luck...)