基于范围的 for 循环,第一项有特殊情况
Range-based for loop with special case for the first item
我发现自己经常使用如下代码:
bool isFirst = true;
for(const auto &item: items)
{
if(!isFirst)
{
// Do something
}
// Normal processing
isFirst = false;
}
似乎应该有更好的方式来表达这一点,因为它是函数中的一种常见模式,类似于“连接”。
你无法知道在基于范围的 for 循环中你访问的是哪个元素,除非你在像 array
或 vector
这样的容器上循环,你可以在其中获取对象的地址并将其与第一项的地址进行比较,以确定您在容器中的位置。如果容器提供按值查找,您也可以这样做,您可以查看从查找操作返回的迭代器是否与 begin
迭代器相同。
如果您需要对第一个元素进行特殊处理,那么您可以退回到传统的 for 循环,例如
for (auto it = std::begin(items), first = it, end = std::end(items); it != end; ++it)
{
if (it == first)
{
// do something
}
// Normal processing
}
如果您需要做的事情可以从循环中分解出来,那么您可以使用基于范围的 for 循环,并将处理放在循环之前,例如
// do something
for(const auto &item: items)
{
// Normal processing
}
一个有趣的替代解决方案是使用自定义迭代器,如果不小心我不会在生产中使用它。
int main() {
std::vector<int> v{1,2,3,4};
for (const auto & [is_first,b] : wrap(v)) {
if (is_first) {
std::cout << "First: ";
}
std::cout << b << std::endl;
}
}
玩具实现可能如下所示:
template<typename T>
struct collection_wrap {
collection_wrap(T &c): c_(c) {}
struct magic_iterator {
bool is_first = false;
typename T::iterator itr;
auto operator*() {
return std::make_tuple(is_first, *itr);
}
magic_iterator operator++() {
magic_iterator self = *this;
itr++;
//only works for forward
is_first = false;
return self;
}
bool operator!=(const magic_iterator &o) {
return itr != o.itr;
}
};
magic_iterator begin() {
magic_iterator itr;
itr.is_first = true;
itr.itr = c_.begin();
return itr;
}
magic_iterator end() {
magic_iterator itr;
itr.is_first = false;
itr.itr = c_.end();
return itr;
}
T &c_;
};
template<typename Collection>
collection_wrap<Collection>
wrap(Collection &vec) {
return collection_wrap(vec);
}
我假设您想知道如何检索第一个元素,您可以使用 array
和 vector
。
我要在这里显示array
。
首先将其包含在您的代码中:
#include <array>
然后相应地转换你的数组:
std::array<std::string, 4> items={"test1", "test2", "test3", "test4"};
for(const auto &item: items)
{
if(item == items.front()){
// do something
printf("\nFirst: %s\n", item.c_str()); //or simply printf("\nFirst:"); since you gonna output a double element
}
// Normal processing
printf("Item: %s\n", item.c_str());
}
return 0;
}
也许 for_first_then_each
就是您要找的东西?它根据迭代器获取您的范围,并将第一个函数应用于第一个元素,将第二个函数应用于其余元素。
#include <iostream>
#include <vector>
template<typename BeginIt, typename EndIt, typename FirstFun, typename OthersFun>
void for_first_then_each(BeginIt begin, EndIt end, FirstFun firstFun, OthersFun othersFun) {
if(begin == end) return;
firstFun(*begin);
for(auto it = std::next(begin); it != end; ++it) {
othersFun(*it);
};
}
int main() {
std::vector<int> v = {0, 1, 2, 3};
for_first_then_each(v.begin(), v.end(),
[](auto first) { std::cout << first + 42 << '\n'; },
[](auto other) { std::cout << other - 42 << '\n'; }
);
// Outputs 42, -41, -40, -39
return 0;
}
随着 C++20 中的 Ranges,您可以将其分成两个循环:
for (auto const& item : items | view::take(1)) {
// first element only (or never executed if items is empty)
}
for (auto const& item : items | view::drop(1)) {
// all after the first (or never executed if items has 1 item or fewer)
}
如果您不想等待 C++20,请查看支持这两种操作的 range-v3。
对于输入范围(例如 items
确实是从 cin
读取的范围),这不会像这样工作,但对于向前或更好的任何范围都可以正常工作(我猜 items
是这里的一个容器,所以应该没问题)。
一个更直接的版本实际上是使用enumerate
(它只存在于range-v3中,不存在于C++20中):
for (auto const& [idx, item] : view::enumerate(items)) {
if (idx == 0) {
// first element only
}
// all elements
}
在 C++ 中仍然有效的一种方法是使用宏:
#include <iostream>
#include <vector>
#define FOR(index, element, collection, body) { \
auto &&col = collection; \
typeof(col.size()) index = 0; \
for(auto it=col.begin(); it!=col.end(); index++, it++) { \
const auto &element = *it; \
body; \
} \
}
using namespace std;
int main() {
vector<int> a{0, 1, 2, 3};
FOR(i, e, a, {
if(i) cout << ", ";
cout << e;
})
cout << endl;
FOR(i, e, vector<int>({0, 1, 2, 3}), {
if(i) cout << ", ";
cout << e;
})
cout << endl;
return 0;
}
打印:
0, 1, 2, 3
0, 1, 2, 3
与其他选项相比,此解决方案简洁明了。不利的一面是,index
正在循环的每次迭代中进行测试和递增 - 这可以通过增加宏的复杂性和使用 bool first
而不是 index
来避免,但是使用宏中的 index
涵盖了比 bool first
.
更多的用例
检查对象地址是否为第一项:
for(const auto &item: items)
{
if (&item != &(*items.begin())
{
// do something for all but the first
}
// Normal processing
}
因为 C++20, you can slightly improve your range-based for loop 使用 init 语句。 init 语句允许您将 isFirst
标志移动到循环范围内,以便该标志在循环外不再可见:
std::vector<int> items { 1, 2, 3 };
for(bool isFirst(true); const auto &item: items) {
if(!isFirst) {
std::cout << "Do something with: " << item << std::endl;
}
std::cout << "Normal processing: " << item << std::endl;
isFirst = false;
}
输出:
Normal processing: 1
Do something with: 2
Normal processing: 2
Do something with: 3
Normal processing: 3
我发现自己经常使用如下代码:
bool isFirst = true;
for(const auto &item: items)
{
if(!isFirst)
{
// Do something
}
// Normal processing
isFirst = false;
}
似乎应该有更好的方式来表达这一点,因为它是函数中的一种常见模式,类似于“连接”。
你无法知道在基于范围的 for 循环中你访问的是哪个元素,除非你在像 array
或 vector
这样的容器上循环,你可以在其中获取对象的地址并将其与第一项的地址进行比较,以确定您在容器中的位置。如果容器提供按值查找,您也可以这样做,您可以查看从查找操作返回的迭代器是否与 begin
迭代器相同。
如果您需要对第一个元素进行特殊处理,那么您可以退回到传统的 for 循环,例如
for (auto it = std::begin(items), first = it, end = std::end(items); it != end; ++it)
{
if (it == first)
{
// do something
}
// Normal processing
}
如果您需要做的事情可以从循环中分解出来,那么您可以使用基于范围的 for 循环,并将处理放在循环之前,例如
// do something
for(const auto &item: items)
{
// Normal processing
}
一个有趣的替代解决方案是使用自定义迭代器,如果不小心我不会在生产中使用它。
int main() {
std::vector<int> v{1,2,3,4};
for (const auto & [is_first,b] : wrap(v)) {
if (is_first) {
std::cout << "First: ";
}
std::cout << b << std::endl;
}
}
玩具实现可能如下所示:
template<typename T>
struct collection_wrap {
collection_wrap(T &c): c_(c) {}
struct magic_iterator {
bool is_first = false;
typename T::iterator itr;
auto operator*() {
return std::make_tuple(is_first, *itr);
}
magic_iterator operator++() {
magic_iterator self = *this;
itr++;
//only works for forward
is_first = false;
return self;
}
bool operator!=(const magic_iterator &o) {
return itr != o.itr;
}
};
magic_iterator begin() {
magic_iterator itr;
itr.is_first = true;
itr.itr = c_.begin();
return itr;
}
magic_iterator end() {
magic_iterator itr;
itr.is_first = false;
itr.itr = c_.end();
return itr;
}
T &c_;
};
template<typename Collection>
collection_wrap<Collection>
wrap(Collection &vec) {
return collection_wrap(vec);
}
我假设您想知道如何检索第一个元素,您可以使用 array
和 vector
。
我要在这里显示array
。
首先将其包含在您的代码中:
#include <array>
然后相应地转换你的数组:
std::array<std::string, 4> items={"test1", "test2", "test3", "test4"};
for(const auto &item: items)
{
if(item == items.front()){
// do something
printf("\nFirst: %s\n", item.c_str()); //or simply printf("\nFirst:"); since you gonna output a double element
}
// Normal processing
printf("Item: %s\n", item.c_str());
}
return 0;
}
也许 for_first_then_each
就是您要找的东西?它根据迭代器获取您的范围,并将第一个函数应用于第一个元素,将第二个函数应用于其余元素。
#include <iostream>
#include <vector>
template<typename BeginIt, typename EndIt, typename FirstFun, typename OthersFun>
void for_first_then_each(BeginIt begin, EndIt end, FirstFun firstFun, OthersFun othersFun) {
if(begin == end) return;
firstFun(*begin);
for(auto it = std::next(begin); it != end; ++it) {
othersFun(*it);
};
}
int main() {
std::vector<int> v = {0, 1, 2, 3};
for_first_then_each(v.begin(), v.end(),
[](auto first) { std::cout << first + 42 << '\n'; },
[](auto other) { std::cout << other - 42 << '\n'; }
);
// Outputs 42, -41, -40, -39
return 0;
}
随着 C++20 中的 Ranges,您可以将其分成两个循环:
for (auto const& item : items | view::take(1)) {
// first element only (or never executed if items is empty)
}
for (auto const& item : items | view::drop(1)) {
// all after the first (or never executed if items has 1 item or fewer)
}
如果您不想等待 C++20,请查看支持这两种操作的 range-v3。
对于输入范围(例如 items
确实是从 cin
读取的范围),这不会像这样工作,但对于向前或更好的任何范围都可以正常工作(我猜 items
是这里的一个容器,所以应该没问题)。
一个更直接的版本实际上是使用enumerate
(它只存在于range-v3中,不存在于C++20中):
for (auto const& [idx, item] : view::enumerate(items)) {
if (idx == 0) {
// first element only
}
// all elements
}
在 C++ 中仍然有效的一种方法是使用宏:
#include <iostream>
#include <vector>
#define FOR(index, element, collection, body) { \
auto &&col = collection; \
typeof(col.size()) index = 0; \
for(auto it=col.begin(); it!=col.end(); index++, it++) { \
const auto &element = *it; \
body; \
} \
}
using namespace std;
int main() {
vector<int> a{0, 1, 2, 3};
FOR(i, e, a, {
if(i) cout << ", ";
cout << e;
})
cout << endl;
FOR(i, e, vector<int>({0, 1, 2, 3}), {
if(i) cout << ", ";
cout << e;
})
cout << endl;
return 0;
}
打印:
0, 1, 2, 3
0, 1, 2, 3
与其他选项相比,此解决方案简洁明了。不利的一面是,index
正在循环的每次迭代中进行测试和递增 - 这可以通过增加宏的复杂性和使用 bool first
而不是 index
来避免,但是使用宏中的 index
涵盖了比 bool first
.
检查对象地址是否为第一项:
for(const auto &item: items)
{
if (&item != &(*items.begin())
{
// do something for all but the first
}
// Normal processing
}
因为 C++20, you can slightly improve your range-based for loop 使用 init 语句。 init 语句允许您将 isFirst
标志移动到循环范围内,以便该标志在循环外不再可见:
std::vector<int> items { 1, 2, 3 };
for(bool isFirst(true); const auto &item: items) {
if(!isFirst) {
std::cout << "Do something with: " << item << std::endl;
}
std::cout << "Normal processing: " << item << std::endl;
isFirst = false;
}
输出:
Normal processing: 1
Do something with: 2
Normal processing: 2
Do something with: 3
Normal processing: 3