除了递增语句,如何使for循环变量const?


82

考虑一个标准的for循环:

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

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

但是,我无法声明iconst因为这会使增量语句无效。有没有一种方法,使i一个const增量声明的变量外?


4
我相信没有办法做到
Itay,

27
这听起来像是寻找问题的解决方案。
皮特·贝克尔

14
将for循环的主体转换为带有const int i参数的函数。索引的可变性仅在需要的地方公开,您可以使用inline关键字使其对编译后的输出不起作用。
蒙蒂·蒂博

4
除...之外,还有什么(或更确切地说,谁)可能会改变索引的值?你不信任自己吗?也许是同事?我同意@PeteBecker。
Z4层

4
@ Z4-tier是的,我当然不信任自己。我知道我会犯错。每个优秀的程序员都知道。这就是为什么我们有const开始的原因。
康拉德·鲁道夫

Answers:


119

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

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

这是一个演示


从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

这是一个演示

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


几乎需要特殊的for循环构造,因为这提供了一种非常非常通用的构造的安全替代方案。
Michael Dorgan

2
@MichaelDorgan好吧,现在已经有了对该功能的库支持,因此将它添加为核心语言功能是不值得的。
cigien

1
不错,尽管我几乎所有真正的工作最多仍是C或C ++ 11。我研究以防万一将来对我很重要...
Michael Dorgan

9
随lambda添加的C ++ 11技巧很简洁,但是在我去过的大多数工作场所中都不实用。静态分析会抱怨通用&捕获,这将强制显式捕获每个引用-这使得它相当麻烦的 我还怀疑这可能导致容易产生的错误,导致作者忘记了(),从而使代码永不被调用。这很容易小到足以在代码审查中丢失。
人类编译器

1
@cigien诸如SonarQube和cppcheck标记之类的静态分析工具通常会捕获,[&]因为它们与AUTOSAR(规则A5-1-2),HIC ++和我认为MISRA(不确定)的编码标准冲突。这并不是说它是不正确的。就是组织禁止此类代码符合标准。至于(),最新的gcc版本甚至都没有标记这个-Wextra。我仍然认为这种方法很整洁。对于许多组织而言,它只是行不通的。
人类编译器

44

对于喜欢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 ++ 11/14等早期版本的C ++,您可能需要删除constexpr那些版本中不合法的位置。

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

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

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

这适用于任何符合C ++ 11的编译器(例如gcc-4.9.4),并且仍会产生基本for-loop副本几乎相同的汇编

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


3
它需要一些样板,但实际上,它所做的并不是那么复杂。它实际上只是一个基本的迭代器模式,但是包装int,然后创建一个“范围”类以返回开始/结束
Human-Compiler

1
不是很重要,但是我还添加了一个c ++ 11解决方案,没有人发布过,所以您可能要稍微改一下答案的第一行:)
cigien

我不确定谁投票赞成,但是如果您认为我的回答不令人满意,以便我可以改善它,我将不胜感激。不赞成投票是表明您认为答案不能充分解决问题的一种好方法,但是在这种情况下,答案中没有现有的批评或明显的错误,我可以改善。
人类编译器

@人编译器我在同一时间也一台DV,为什么无论是:(猜有人不喜欢的范围抽象他们并没有发表评论,我不会担心它。
cigien

1
“ assembly”是一个质量名词,如“行李”或“水”。正常的措词是“将编译为与C ++ 20相同的程序集……”。编译器的输出汇编为一个单一的功能不是一个单数组件,它的“组件”(的汇编语言指令序列)。
彼得·科德斯

29

吻版本...

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

    // use i here
}

如果您的用例只是为了防止意外修改循环索引,则应该使此类错误显而易见。(如果您想防止故意修改,祝您好运...)


11
我认为您教错了使用以开头的魔术标识符的课程_。并作一些解释(例如范围)会有所帮助。否则,是的,很好。
Yunnosch

14
调用“ hidden”变量i_会更合规。
尔卡

9
我不确定这如何回答问题。循环变量是_i仍可在循环中修改的变量。
cigien

4
@cigien:IMO,如果没有C ++ 20std::views::iota以完全防弹的方式使用,那么此局部解决方案是值得的。答案的文字说明了其局限性以及它如何尝试回答问题。从易于阅读,易于维护的IMO角度来看,一堆过于复杂的C ++ 11使这种疗法比疾病更糟糕。对于每个了解C ++的人来说,这仍然很容易阅读,并且作为一种习惯用法似乎很合理。(但应避免使用下划线开头的名称。)
Peter Cordes

5
仅@Yunnosch _Uppercasedouble__underscore标识符保留。_lowercase标识符仅在全局范围内保留。
Roman Odaisky

13

如果您无权访问 ,使用功能进行典型的改造

#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的评论中得到启发,我想知道天气给出的答案在性能方面有什么不同。事实证明,除了这种方法之外,所有其他方法都具有令人惊讶的相同性能(针对range [0, 10))。这种std::vector方法是最糟糕的。

在此处输入图片说明

请参阅在线快速测试台


4
尽管此方法适用于c ++ 20之前的版本,但由于需要使用,因此开销非常大vector。如果范围很大,可能会很糟糕。
人类编译器

@ Human-Compiler:std::vector如果范围很小,A在相对规模上也是相当糟糕的,如果这应该是运行了很多次的小内循环,则可能会非常糟糕。一些编译器(例如带有libc ++的clang,但不是libstdc ++)可以优化无法逃避该功能的分配的新/删除,但是,否则,这很容易成为小的完全展开循环与对new+的调用之间的区别delete,并且实际上可能存储在该内存中。
Peter Cordes

IMO,const i如果没有C ++ 20便宜的方法,在大多数情况下,次要的好处就是不值得开销。尤其是使用运行时变量范围,这使编译器不太可能优化所有内容。
Peter Cordes

13

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

它不如提出的某些解决方案最佳,但是如果可能的话,这样做非常简单。

编辑:只是一个例子,我倾向于不清楚。

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

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

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
如果最大数量由运行时值决定,则此比例不缩放。
人类编译器

12
@ Human-Compiler只需将列表扩展到所需的值,然后动态地重新编译整个程序即可;)
Monty Thibault

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 ++的“可变阴影”使标识符解析为引用原始step变量的const ref变量,是一种很好的解决方案。或者至少是最优雅的一款。
Max Barraclough

4

在任何版本的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),但仍会起作用。

这是一个演示


尽管这对于C ++中的for循环可能不是惯用的,但是这种方法在其他语言中非常普遍。功能性包装程序在复杂语句中的组合性确实非常时尚,使用起来非常符合人体工程学。

该代码也易于编写,理解和维护。


使用此方法要注意的一个局限性是某些组织禁止lambda(例如[&][=])上的默认捕获以符合某些安全标准,这可能会使lambda膨胀,而每个成员都需要手动捕获。并非所有组织都这样做,因此我仅将其作为评论而不是答案提及。
人类编译器,

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.