除了 increment 语句之外,如何使 for 循环变量为 const?

How to make a for loop variable const with the exception of the increment statement?

考虑一个标准的 for 循环:

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

我想防止变量 ifor 循环体中被修改。

但是,我不能将 i 声明为 const,因为这会使增量语句无效。有没有办法在增量语句之外使 i 成为 const 变量?

从 c++20 开始,您可以像这样使用 ranges::views::iota

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

这是一个demo


从 c++11 开始,您还可以使用以下技术,它使用 IIILE(立即调用内联 lambda 表达式):

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

这里是 demo

请注意,[&,i] 意味着 i 被 non-mutable 副本捕获,其他所有内容都被可变引用捕获。循环末尾的 (); 仅表示立即调用 lambda。

这里是 C++11 版本:

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

Here is live demo

如果您无法访问 ,使用函数

进行典型改造
#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
}

(See a Demo)


更新:受@Human-Compiler的评论启发,我想知道给定的答案是否有任何不同在性能的情况下。事实证明,除了这种方法之外,所有其他方法都出人意料地具有相同的性能(对于 [0, 10) 范围)。 std::vector 方法是最糟糕的。

(See Online Quick-Bench)

对于喜欢 Cigien 的 std::views::iota 答案但不在 C++20 或更高版本中工作的任何人,实现 std::views::iota 兼容 [=30= 的简化和轻量级版本相当简单] 或以上。

它只需要:

  • 基本的“LegacyInputIterator”类型(定义 operator++operator* 的东西),它包含一个整数值(例如 int
  • 一些“范围”-like class 具有 begin()end() returns 上述迭代器。这将允许它在 range-based 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在这些版本中这样做。

以上样板文件使以下代码能够在 pre-C++20:

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

优化后将生成 same assembly 作为 C++20 std::views::iota 解决方案和 classic for 循环解决方案。

这适用于任何 C++11 兼容的编译器(例如像 gcc-4.9.4 这样的编译器)并且仍然会生成 nearly identical assembly 到基本的 for 循环对应物。

注意: iota 辅助函数仅适用于 feature-parity 和 C++20 std::views::iota 解决方案;但实际上,您也可以直接构造一个 iota_range{...} 而不是调用 iota(...)。如果用户希望将来切换到 C++20,前者只是提供了一个简单的升级路径。

KISS 版本...

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

    // use i here
}

如果您的用例只是为了防止意外修改循环索引,那么这样的错误应该很明显。 (如果你想防止故意修改,好吧,祝你好运...)

#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(...) header 中声明的变量被认为与在 header 中声明的变量处于同一嵌套级别{...} 复合语句。这意味着,例如:

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

什么?我们不是刚刚打开了一个花括号吗?而且,它不一致:

void fun(int i)
{
  int i = 42; // OK
}

你不能在接受 i 作为常量的函数中移动 for 循环的部分或全部内容吗?

它不如一些建议的解决方案最优,但如果可能的话,这很容易做到。

编辑:只是一个例子,因为我往往不清楚。

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

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

此处尚未提及的一种适用于任何版本的 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 的东西都会被卡住,将 strongly-named 函数指针传递给 for_each(类似于 std::for_each),但它仍然有效。

这是一个demo


虽然这对于 C++ 中的 for 循环可能不是惯用的,但这种方法在其他语言中很常见。函数式包装器因其在复杂语句中的可组合性而非常时尚,并且使用起来非常符合人体工程学。

此代码也易于编写、理解和维护。

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