除了 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
}
我想防止变量 i
在 for
循环体中被修改。
但是,我不能将 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
}
如果您无法访问 c++20,使用函数
进行典型改造
#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的评论启发,我想知道给定的答案是否有任何不同在性能的情况下。事实证明,除了这种方法之外,所有其他方法都出人意料地具有相同的性能(对于 [0, 10)
范围)。 std::vector
方法是最糟糕的。
对于喜欢 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
考虑一个标准的 for 循环:
for (int i = 0; i < 10; ++i)
{
// do something with i
}
我想防止变量 i
在 for
循环体中被修改。
但是,我不能将 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
}
如果您无法访问 c++20,使用函数
进行典型改造#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的评论启发,我想知道给定的答案是否有任何不同在性能的情况下。事实证明,除了这种方法之外,所有其他方法都出人意料地具有相同的性能(对于 [0, 10)
范围)。 std::vector
方法是最糟糕的。
对于喜欢 Cigien 的 std::views::iota
答案但不在 C++20 或更高版本中工作的任何人,实现 std::views::iota
兼容 [=30= 的简化和轻量级版本相当简单] 或以上。
它只需要:
- 基本的“LegacyInputIterator”类型(定义
operator++
和operator*
的东西),它包含一个整数值(例如int
) - 一些“范围”-like class 具有
begin()
和end()
returns 上述迭代器。这将允许它在 range-basedfor
循环中工作
这个的简化版本可以是:
#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