【问题标题】:Range-based for loop with special case for the first item基于范围的 for 循环,第一项的特殊情况
【发布时间】:2019-07-16 20:04:20
【问题描述】:

我发现自己经常使用如下代码:

bool isFirst = true;
for(const auto &item: items)
{
    if(!isFirst) 
    { 
       // Do something
    }
    // Normal processing
    isFirst = false;
}

似乎应该有更好的方式来表达这一点,因为它是函数中的一种常见模式,就像“连接”一样。

【问题讨论】:

  • 这就是为什么我很伤心bools 没有-- 了...
  • 不应该是else { /* Normal processing */ }吗?在这种情况下,您可以查看此答案(它跳过了最后一个元素,想法类似):stackoverflow.com/a/35372958/2956272
  • 当我的代码需要特殊的第一个元素操作时(坦率地说,我可以用一只手计算过去五年中我需要它的次数并且仍然有手指剩余),我不使用远程。我使用迭代器、if-test 和嵌套的 while 循环。唯一引起头痛的地方是当所采取的行动是先行而不是替代时(即您正在使用的模型是这样的情况:第一个元素得到动作 A+B,其余元素得到 B,而不是第一个元素得到A,剩下的得到 B)。
  • 谢谢大家。我认为在stackoverflow.com/questions/21215947/… 上发帖的@Ali 有很好的观点。将特殊情况移出循环是一个更好的主意。

标签: c++ for-loop c++11 range-based-loop


【解决方案1】:

也许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;
}

【讨论】:

  • 我最喜欢这个。这是最有效的,而且您没有在循环中进行检查。
【解决方案2】:

您无法知道您在基于范围的 for 循环中访问的是哪个元素,除非您在 arrayvector 之类的容器上循环,您可以在其中获取对象的地址并将其与地址进行比较找出你在容器中的位置的第一项。如果容器提供了按值查找,也可以这样做,可以查看查找操作返回的迭代器是否与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
}

【讨论】:

  • 您的第一个示例可以很好地包装到一个(算法)模板中,该模板采用一个迭代器范围和两个 std::functions(FirstElementProcessor、OtherElementProcessor)。
  • 我想过一个模板,但我想知道是否已经有类似的东西,因为它似乎是一种常见的模式。
【解决方案3】:

一个有趣的替代解决方案是使用自定义迭代器。

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);
}

【讨论】:

  • 方括号是“结构化绑定”吗?
  • 您应该添加template&lt;typename T&gt; void warp(T&amp;&amp; t) = delete; 以防止使用悬空引用
【解决方案4】:

随着 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 读取的范围),但对于任何向前或更好的范围都可以正常工作(我猜 @ 987654325@ 是这里的容器,应该没问题)。


更直接的版本其实是使用enumerate(只存在于range-v3中,不存在于C++20中):

for (auto const& [idx, item] : view::enumerate(items)) {
    if (idx == 0) {
         // first element only
    }
    // all elements
}

【讨论】:

  • 我喜欢展开循环。即开始,检查它是否没有结束,如果是,则处理第一项,增加迭代器然后完成迭代。将整个内容包装在模板函数中。
  • @bpeikes 当然——答案是存在的。这只是一种替代方法。有一些缺点,也有一些好处(例如,您可以在第一个循环中使用return,在第二个循环中使用break)。
【解决方案5】:

检查对象地址是否为第一项:

for(const auto &item: items)
{
    if (&item != &(*items.begin())
    { 
       // do something for all but the first
    }
    // Normal processing
}

【讨论】:

  • 如果不是美的东西,我觉得这是最不冒犯的解决方案。我想说“如果(/*不是第一次迭代*/)”,这很接近。简短的评论使读者免于需要太多专业知识。我的逗号位置谢谢!
【解决方案6】:

在 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更多的用例。

【讨论】:

  • 这是一个模板函数的完美案例,你为什么要使用一个宏,它可能会出现各种边缘情况失败?
  • @bpeikes 这是一种有效的方法。就这些。不想用就不用了。
  • @bpeikes 在没有 C++ 概念的情况下,模板在某种程度上基于鸭子类型(如宏)。写到这里,我提倡使用宏而不是模板。
【解决方案7】:

由于C++20,您可以通过使用初始化语句稍微改进您的range-based for loop。 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;
}

输出:

正常处理:1
做一些事情:2
正常处理:2
做点什么:3
正常处理:3

Code on Wandbox

【讨论】:

    【解决方案8】:

    我假设您想知道如何检索第一个元素,您可以使用 arrayvector 来做到这一点。

    我将在此处显示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;
    }
    

    【讨论】:

    • 为什么在 c++ 代码中使用printf()“性能”货物培养?
    • @πάνταῥεῖ:我在 C++ 代码中一直使用printf。我被允许,我更喜欢它,而且它有效,那么你是谁来评判我?
    • 并非所有可用于基于范围的 for 循环的项目容器都必须实现对 front() 的访问
    • @πάνταῥεῖ 是的,我更喜欢 printf,你能做什么。不知道人们对此表示反对。 :D
    • @Secko,否决票不是开玩笑。您比较值item == items.front()。不保证第一个元素不重复!
    猜你喜欢
    • 2016-10-31
    • 1970-01-01
    • 2014-12-06
    • 2014-01-12
    • 2013-01-04
    • 1970-01-01
    • 1970-01-01
    • 2014-10-28
    • 1970-01-01
    相关资源
    最近更新 更多