使用索引与迭代器将向量迭代到倒数第二个元素

Iterating a vector to second to last element with index vs iterator

本文关键字:倒数 倒数第二 第二个 元素 迭代 向量 索引 迭代器      更新时间:2023-10-16

从 C++11std::vector的开头迭代到倒数第二个元素时,首选样式是什么?

std::vector<const char*> argv;
std::string str;

是否应该使用这种更C++式的方法

for (const auto& s: decltype(argv)(argv.begin(), argv.end()-1)) {
str += std::string(s) + ' ';
}

还是应该首选更传统的方式?

for (size_t i = 0; i < argv.size() - 1; ++i) {
str += std::string(argv[i]);
}

请不要写这个:

for (const auto& s: decltype(argv)(argv.begin(), argv.end()-1)) {

首先,当你回顾它时,没有人(包括你(会理解这一点。其次,由于decltype(argv)是一个vector,这是复制一大堆元素......所有这些都只是因为您想避免迭代其中之一?这是非常浪费的。

它还有另一个问题,您的第二个选项共享该问题。

这:

for (size_t i = 0; i < argv.size() - 1; ++i) {

问题要微妙得多,因为size()是未签名的。因此,如果argv恰好是空的,argv.size() - 1将是一个非常大的数字,您实际上将访问数组中所有这些无效元素,从而导致未定义的行为。

对于迭代器,如果argv.begin() == argv.end(),则无法从end()获取以前的迭代器,因为没有来自end()的先前迭代器。所有end() - 1prev(end())--end()已经是未定义的行为。在这一点上,我们甚至无法推理循环将做什么,因为我们甚至没有有效的范围。


我的建议是:

template <typename It>
struct iterator_pair {
It b, e;
It begin() const { return b; }
It end() const { return e; }
};
// this doesn't work for types like raw arrays, I leave that as
// an exercise to the reader
template <typename Range>
auto drop_last(Range& r) 
-> iterator_pair<decltype(r.begin())>
{
return {r.begin(), r.begin() == r.end() ? r.end() : std::prev(r.end())};
}

这使您可以执行以下操作:

for (const auto& s : drop_last(argv)) { ... }

这是有效的(避免额外的副本(,避免未定义的行为(drop_last()总是给出一个有效的范围(,并且从名称中可以清楚地看出它的作用。

我发现第一个选项阅读起来有点笨拙,但由于这是个人喜好的问题,我提出了一种避免手写循环的替代方法(并且假设argv.size() >= 1(,从某种意义上说,这可能更好,因为它减少了拼写错误和索引错误的可能性。

#include <numeric>
std::string str;
if (!argv.empty())
str = std::accumulate(argv.begin(), std::prev(argv.end()), str);

我的建议是在您的项目中包含指南支持库,如果不是更通用的范围库,然后使用gsl::span(等待 C++20 获取它,因为std::span可能有点长(或类似的东西来访问您想要的子范围。

此外,它可能很小,但它足够复杂,可以保证它自己的功能:

template <class T>
constexpr gsl::span<T> drop_last(gsl::span<T> s, gsl::span<T>::index_type n = 1) noexcept
{ return s.subspan(0, std::min(s.size(), n) - n); }
for (auto s : drop_last(argv)) {
// do things
}

实际上,强烈建议查看范围和视图的效率(减少间接性,无复制(和解耦(被调用方不再需要知道使用的确切容器(。

另一种选择是将std::for_each与lambda一起使用。

std::for_each(argv.begin(), std::prev(argv.end()), [&](const auto& s){ str += s; });

看起来您正在尝试输出容器中的元素,它们之间有一个空格。 另一种写法是:

const char* space = "";     // no space before the first item
for (const char* s : argv) {
str += space;
str += s;
space = " ";
}

要解决您的特定示例,您可以使用Google Abseil。

来自 str_join.h 标头的代码示例:

std::vector<std::string> v = {"foo", "bar", "baz"};
std::string s = absl::StrJoin(v, "-");
EXPECT_EQ("foo-bar-baz", s);

EXPECT_EQ是谷歌测试宏,这意味着s等于"foo-bar-baz"

我要扮演魔鬼的代言人,说只是argv.size()int并使用常规的 for 循环。这大概在99%的情况下都有效,易于阅读,并且不需要深奥的C++知识即可理解。

for(int i=0; i<(int)argv.size() - 1; i++)

如果您的容器有超过 20 亿个元素,您可能会提前知道这一点,在这种情况下,只需使用size_t并使用特殊情况来检查argv.size() == 0以防止下溢。

我建议使用std::prev(v.end())而不是v.end()-1。这更惯用,也适用于其他容器。

下面是一个演示这个想法的main函数。

int main()
{
std::set<int> s = {1, 2, 3, 4};
for (const auto& item: decltype(s)(s.begin(), std::prev(s.end())))
{
std::cout << item << std::endl;
}
std::vector<int> v = {10, 20, 30};
for (const auto& item: decltype(v)(v.begin(), std::prev(v.end())))
{
std::cout << item << std::endl;
}
}

请注意,上面的代码构造了临时容器。当容器很大时,构造临时容器将是一个性能问题。对于这种情况,请使用简单的for循环。

for (auto iter = argv.begin(); iter != std::prev(argv.end()); ++iter )
{
str += *iter + ' ';
}

或使用std::for_each.

std::for_each(argv.begin(), std::prev(argv.end()),
[](std::string const& item) { str += item + ' '; });