除了 increment 语句外,如何制作 for 循环变量 const?

jho*_*ack 83 c++ algorithm variables for-loop constants

考虑一个标准 for 循环:

for (int i = 0; i < 10; ++i) 
{
   // do something with i
}
Run Code Online (Sandbox Code Playgroud)

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

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

cig*_*ien 119

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

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

这是一个演示


从 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
Run Code Online (Sandbox Code Playgroud)

这是一个演示

请注意,这[&,i]意味着i由非可变副本捕获,其他所有内容均由可变引用捕获。该();在循环的结束只是意味着该拉姆达立即调用。

  • 您使用 lambda 添加的 C++11 技巧很简洁,但在我经历过的大多数工作场所中并不实用。静态分析会抱怨广义的“&amp;”捕获,这将强制显式捕获每个引用 - 这使得这相当麻烦。我还怀疑这可能会导致简单的错误,即作者忘记了“()”,从而使代码永远不会被调用。这很容易在代码审查中被忽略。 (9认同)
  • @MichaelDorgan 好吧,既然有了对此功能的库支持,那么将其添加为核心语言功能就不值得了。 (2认同)

Hum*_*ler 45

对于喜欢 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};
}
Run Code Online (Sandbox Code Playgroud)

我已经在上面定义了constexpr它支持的地方,但是对于像 C++11/14 这样的早期 C++ 版本,您可能需要删除constexpr在这些版本中不合法的地方。

上面的样板使以下代码能够在 C++20 之前的版本中工作:

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

优化时将生成C++20std::views::iota解决方案和经典for-loop 解决方案相同的程序集

这适用于任何符合 C++11 的编译器(例如像 的编译器gcc-4.9.4),并且仍然生成基本for-loop 对应物几乎相同的程序集

注意:iota辅助函数仅仅是特征奇偶与C ++ 20std::views::iota溶液; 但实际上,您也可以直接构造一个iota_range{...}而不是调用iota(...). 如果用户希望将来切换到 C++20,前者只是提供了一个简单的升级路径。

  • 它需要一些样板文件,但实际上它所做的事情并不那么复杂。它实际上只是一个基本的迭代器模式,但是包装了一个“int”,然后创建一个“range”类来返回开始/结束 (3认同)

Art*_*ius 29

亲吻版本...

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

    // use i here
}
Run Code Online (Sandbox Code Playgroud)

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

  • 调用“隐藏”变量“i_”会更合规。 (14认同)
  • 我认为您使用以“_”开头的神奇标识符教了错误的课程。一些解释(例如范围)会有所帮助。否则,是的,很好的KISSy。 (11认同)
  • 我不确定这如何回答这个问题。循环变量是“_i”,它在循环中仍然可以修改。 (10认同)
  • @Yunnosch 仅保留“_Uppercase”和“double__underscore”标识符。`_lowercase` 标识符仅在全局范围内保留。 (5认同)
  • @cigien:IMO,这个部分解决方案在没有 C++20 `std::views::iota` 的情况下是值得的,以实现完全防弹的方式。答案的文本解释了其局限性以及它如何尝试回答问题。在我看来,一堆过于复杂的 C++11 在易于阅读、易于维护方面使治疗方法比疾病更糟糕。对于每个了解 C++ 的人来说,这仍然很容易阅读,并且作为一个习惯用法似乎很合理。(但应避免使用前导下划线名称。) (4认同)

Al *_* rl 13

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

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

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

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

void looper ( const int v )
{
    // do your thing here
}
Run Code Online (Sandbox Code Playgroud)


JeJ*_*eJo 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;
}
Run Code Online (Sandbox Code Playgroud)

现在你可以

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

见演示


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

在此处输入图片说明

见在线快速工作台

  • 虽然这适用于 c++20 之前的版本,但由于需要使用“向量”,因此会产生相当大的开销。如果范围很大,这可能会很糟糕。 (5认同)

Vla*_*ein 10

这是一个 C++11 版本:

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

这是现场演示

  • @Human-Compiler 只需将列表扩展到所需的值并动态重新编译整个程序;) (12认同)
  • 如果最大数量由运行时值决定,则此值不会缩放。 (6认同)
  • 您没有提到“{..}”的情况是什么。您需要添加一些内容才能激活此功能。例如,如果您不添加正确的标头,您的代码将会中断:https://godbolt.org/z/esbhra。依赖“&lt;iostream&gt;”来获取其他标头是一个坏主意! (6认同)

Kaz*_*Kaz 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.
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

注意:我们需要嵌套作用域,因为语言中有一个惊人的愚蠢:在for(...)头文件中声明的变量被认为与在{...}复合语句中声明的变量处于同一嵌套级别。这意味着,例如:

for (int i = ...)
{
  int i = 42; // error: i redeclared in same scope
}
Run Code Online (Sandbox Code Playgroud)

什么?我们不是刚打开一个花括号吗?此外,它是不一致的:

void fun(int i)
{
  int i = 42; // OK
}
Run Code Online (Sandbox Code Playgroud)