【问题标题】:How to make a for loop variable const with the exception of the increment statement?除了增量语句之外,如何制作一个 for 循环变量 const?
【发布时间】:2020-12-03 13:51:09
【问题描述】:

考虑一个标准的 for 循环:

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

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

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

【问题讨论】:

  • 我相信没有办法做到这一点
  • 这听起来像是寻找问题的解决方案。
  • 将 for 循环的主体转换为带有 const int i 参数的函数。索引的可变性只在需要的地方公开,您可以使用inline 关键字使其对编译输出没有影响。
  • 除了......你之外,还有什么(或者更确切地说,谁)可能改变索引的值?你不信任自己吗?也许是同事?我同意@PeteBecker。
  • @Z4-tier 是的,我当然不信任自己。我知道我会犯错误。每个优秀的程序员都知道。这就是为什么我们要开始使用 const 这样的东西。

标签: c++ algorithm for-loop variables constants


【解决方案1】:
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

【讨论】:

  • 没有提出 lambda auto const i 的论点似乎真的违背了这一点......!
  • @underscore_d 的重点是使计数器变量在循环中不可修改,在我的示例中为start。是否要i const 取决于您,因为这不是重点,尽管这是一个不错的奖励。
【解决方案2】:

对于喜欢 Cigien 的 std::views::iota 答案但不在 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
}

优化后将生成 same assembly 作为 C++20 std::views::iota 解决方案和经典 for-loop 解决方案。

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

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

【讨论】:

  • 它需要一些样板文件,但实际上它的作用并没有那么复杂。它实际上只是一个基本的迭代器模式,但包装了一个int,然后创建一个“范围”类来返回开始/结束
  • 不是超级重要,但我还添加了一个没有其他人发布的 c++11 解决方案,因此您可能需要稍微改写答案的第一行 :)
  • 我不确定谁投了反对票,但如果您觉得我的回答不令人满意,我会很感激一些反馈,以便我可以改进它。投反对票是一种很好的方式来表明您认为答案并不能充分解决问题,但在这种情况下,答案中没有现有的批评或明显的错误,我可以改进。
  • @Human-Compiler 我同时也得到了一个 DV,他们也没有评论为什么 :( 猜想有人不喜欢范围抽象。我不会担心.
  • “assembly”是一个大众名词,如“luggage”或“water”。正常的措辞是“将编译为与 C++20 相同的 程序集 ...”。单个函数的编译器 asm 输出不是 a 单数汇编,而是“汇编”(汇编语言指令序列)。
【解决方案3】:

这里尚未提到的一种适用于任何 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),但它仍然有效。

这是demo


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

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

【讨论】:

  • 使用这种方法需要注意的一个限制是,一些组织禁止对 lambda 的默认捕获(例如 [&amp;][=])以符合某些安全标准,这可能会使 lambda 膨胀每个成员都需要手动捕获。并非所有组织都这样做,所以我只是将其作为评论而不是在答案中提及。
【解决方案4】:

您不能将 for 循环的部分或全部内容移到一个接受 i 作为 const 的函数中吗?

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

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

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

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

【讨论】:

    【解决方案5】:

    这是一个 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

    【讨论】:

    • 如果最大数量由运行时值决定,则无法扩展。
    • @Human-Compiler 只需将列表扩展到所需的值并动态重新编译整个程序;)
    • 你没有提到{..}的情况。您需要包含一些内容才能激活此功能。例如,如果您不添加正确的标题,您的代码将会中断:godbolt.org/z/esbhra。为其他标头中继 &lt;iostream&gt; 是个坏主意!
    【解决方案6】:

    如果您无权访问,则使用函数进行典型改造

    #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)

    【讨论】:

    • 虽然这适用于 c++20 之前的版本,但由于需要使用 vector,所以开销很大。如果范围非常大,这可能会很糟糕。
    • @Human-Compiler: 如果范围很小,std::vector 在相对规模上非常糟糕,如果这应该是一个运行很多的小内部循环,则可能非常糟糕多次。一些编译器(例如带有 libc++ 的 clang,但不是 libstdc++)可以优化不转义函数的分配的新/删除,否则这很容易成为小型完全展开循环与调用 @ 987654332@ + delete,并且可能实际上存储到那个内存中。
    • IMO,const i 的次要好处在大多数情况下根本不值得开销,如果没有 C++20 使其便宜的方法。特别是对于运行时变量范围,编译器不太可能优化所有内容。
    【解决方案7】:
    #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
    }
    

    【讨论】:

    • 这无疑是最好的答案。利用 C++ 的“变量阴影”使标识符解析为引用原始 step 变量的 const ref 变量,这是一个优雅的解决方案。或者至少,最优雅的一款。
    【解决方案8】:

    从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

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

    【讨论】:

    • 几乎需要一个特殊的 for 循环结构,因为它提供的是一个非常非常常见的结构的更安全替代方案。
    • @MichaelDorgan 好吧,既然库支持此功能,那么将其作为核心语言功能添加是不值得的。
    • 公平,尽管我几乎所有的实际工作最多仍然是 C 或 C++11。我学习是为了以防将来对我很重要......
    • 您在 lambda 中添加的 C++11 技巧很简洁,但在我去过的大多数工作场所都不实用。静态分析会抱怨泛化的 &amp; 捕获,这将强制显式捕获每个引用——这使得这非常麻烦。我还怀疑这可能会导致作者忘记() 的简单错误,从而使代码永远不会被调用。这很容易在代码审查中错过。
    • @cigien 静态分析工具,如SonarQube 和 cppcheck 标志一般捕获,如[&amp;],因为这些与 AUTOSAR(规则 A5-1-2)、HIC++ 等编码标准相冲突,我认为还有 MISRA (不确定)。并不是说它不正确;是组织禁止此类代码符合标准。至于(),最新的gcc version does not flag this 甚至还有-Wextra。我仍然认为这种方法很简洁。它只是不适用于许多组织。
    【解决方案9】:

    KISS 版本...

    for (int _i = 0; _i < 10; ++_i) {
        const int i = _i;
    
        // use i here
    }
    

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

    【讨论】:

    • 我认为你教错了使用以_ 开头的神奇标识符。一些解释(例如范围)会有所帮助。否则,是的,很好 KISSy。
    • 调用“隐藏”变量i_会更合规。
    • 我不确定这如何回答这个问题。循环变量是_i,在循环中仍然可以修改。
    • @cigien:IMO,这个部分解决方案在没有 C++20 std::views::iota 的情况下是值得的,因为它是一种完全防弹的方式。答案的文本解释了它的局限性以及它如何尝试回答问题。一堆过于复杂的 C++11 在易读、易维护、IMO 方面让治愈比疾病更糟糕。对于所有了解 C++ 的人来说,这仍然很容易阅读,并且作为一个成语似乎是合理的。 (但应避免前导下划线名称。)
    • @Yunnosch 仅保留 _Uppercasedouble__underscore 标识符。 _lowercase 标识符仅保留在全局范围内。
    猜你喜欢
    • 2013-06-03
    • 2010-11-16
    • 2021-07-19
    • 2018-12-25
    • 2017-03-22
    • 2019-09-27
    • 1970-01-01
    • 2018-09-28
    • 2020-04-05
    相关资源
    最近更新 更多