使用索引还是迭代器来遍历一个向量到倒数第二个元素?

10

当从 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]);
}

6
除非您检查argv.end()-1和/或argv.size()-1是否有效,否则不要这样做。 - Richard Critten
然后 i + 1 < argv.size() - bipll
@bipll 噢...这是因为size()是无符号的,而0ULL - 1非常大? - chew socks
@OlivierSohn 我认为这个问题会帮助我提高我的C++水平(事实证明确实如此),但是特别针对这种情况,argv的最后一个元素是nulltpr,因为它在execvp(argv[0], (char* const*)&argv[0])中发生了。 - chew socks
@chewsocks 我明白了,谢谢! - Olivier Sohn
显示剩余2条评论
8个回答

14

请不要写这个:

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()始终提供有效范围),从名称上也很清楚它的作用。

“作为读者的练习”……你是教授吗? =P - chew socks
1
请注意,Haskell 中相应的函数称为 init,尽管在 C++ 中,这个名称经常用于相当反模式的代码设计。 - bipll
1
@chewsocks - 我认为对于decltype的一个通用经验法则是,一般情况下应该避免使用它,特别是在模板之外。我自己花了相当长的时间才理解那段代码。永远不应该在可能被轻松解释为函数调用的上下文中使用decltype - Omnifarious
@Omnifarious 你有没有一篇具体的文章,可以比评论中所能容纳的内容更详细地描述这个问题?我想多了解一些关于这个的知识,实际上我一直在尝试使用“decltype”。 - chew socks
1
@chewsocks - 另外,这篇GotW很好:https://herbsutter.com/2013/08/12/gotw-94-solution-aaa-style-almost-always-auto/ - 我必须说很多人有点不同意我的看法。我仍然坚持在容易与函数调用混淆的情况下永远不要使用decltype。在这种情况下,我建议使用usingtypedef声明以及decltype,这样您就有了类型的本地名称,并使用该名称。此外,更喜欢{...}初始化语法,因为它也更难与函数调用混淆。 - Omnifarious
显示剩余3条评论

6

我认为第一种选项有些笨拙难读,但由于这更多是个人偏好的问题,因此我提出了一种替代方法,避免手写循环(并假设 argv.size() >= 1),可能更好的地方在于它减少了打字错误和索引错误的可能性。

#include <numeric>

std::string str;

if (!argv.empty())
    str = std::accumulate(argv.begin(), std::prev(argv.end()), str);

2
很好地运用了 accumulate - NathanOliver
2
“--argv.end()” 不能保证是有效的。另一方面,“argv.end() - 1” 是有效的。 - Barry
@Barry,你能给我一点提示为什么递减它不保证是有效的吗? - lubgr
2
@chewsocks 是的?指针仍然是迭代器。 - Barry
我喜欢这个答案 +1,但我会诱惑将 if(!argv.empty()) 语句放入代码中,这样对于那些比阅读文字更擅长阅读代码的人来说就更清晰了。 :-) - Galik
显示剩余6条评论

4
我的建议是将 指南支持库 包含在您的项目中,如果不是更通用的范围库,然后使用 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
}

实际上,为了提高效率(减少间接引用和复制)和解耦(被调用者不再需要知道使用的确切容器),建议查看范围和视图。

2
另一个选择是使用lambda表达式和std::for_each函数。
std::for_each(argv.begin(), std::prev(argv.end()), [&](const auto& s){ str += s; });

0

看起来您正在尝试在容器中输出元素,并在它们之间留有空格。另一种编写方式是:

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

0

针对您的特定示例,您可以使用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 是 Google Test 的宏,意思是 s 等于 "foo-bar-baz"


-1
我会斗胆提出异议,建议你只需将 argv.size() 转换为 int 并使用普通的 for 循环。这种方法在大约 99% 的情况下都能正常工作,易于阅读,不需要深入了解 C++ 的玄学知识。
for(int i=0; i<(int)argv.size() - 1; i++)

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


-2
我建议使用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 + ' '; });

4
除非你解决创建一个可能很大的容器副本的明显性能问题。 - Omnifarious
@Omnifarious 是否有可能编译器优化掉中间副本 - 也许如果它被声明为const? - chew socks
4
不太可能。副作用太多,包括内存分配和元素复制。编译器几乎不可能证明这些副作用是无关紧要的。 - Omnifarious
std::prev 是 C++11 吗? - stephanmg
@stephanmg,是的,它就是。https://en.cppreference.com/w/cpp/iterator/prev - R Sahu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接