[英]Iterating a vector to second to last element with index vs iterator
从 C++11 std::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()
是unsigned 。 因此,如果argv
恰好为空,则argv.size() - 1
将是一个非常大的数字,并且您实际上将访问数组的所有这些无效元素,从而导致未定义的行为。
对于迭代器,如果argv.begin() == argv.end()
那么你就不能得到以前的迭代器end()
,因为没有来自以前的迭代end()
所有end() - 1
、 prev(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 + ' '; });
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.