更喜欢迭代器而不是指针?
Prefer Iterators Over Pointers?
is a bump of a question that had a comment here 但作为凸起的一部分被删除了。
对于那些看不到已删除帖子的人,评论是关于我在 中使用 const char*
s 而不是 string::const_iterator
s:"Iterators may have been a better path from the get go, since it appears that is exactly how your pointers seems be treated."
所以我的问题是,迭代器是否持有 string::const_iterator
s 持有超过 const char*
s 的任何内在价值,以便将我的答案切换到 string::const_iterators
有意义吗?
简介
使用迭代器代替指针有很多好处,其中包括:
- release 与 debug 中的不同代码路径,以及;
- 更好的类型安全,并且;
- 使编写通用代码成为可能(迭代器可以用于任何数据结构,例如链表,而内部指针在这方面非常有限)。
调试
因为除其他外,取消引用传递到范围末尾的迭代器是 未定义的行为,在这种情况下,实现可以自由地做任何它认为必要的事情- 包括提出诊断说你做错了什么。
标准库实现,libstdc++, provided by gcc will issues diagnostics when it detects something fault (if Debug Mode 已启用)。
例子
#define _GLIBCXX_DEBUG 1 /* enable debug mode */
#include <vector>
#include <iostream>
int
main (int argc, char *argv[])
{
std::vector<int> v1 {1,2,3};
for (auto it = v1.begin (); ; ++it)
std::cout << *it;
}
/usr/include/c++/4.9.2/debug/safe_iterator.h:261:error: attempt to
dereference a past-the-end iterator.
Objects involved in the operation:
iterator "this" @ 0x0x7fff828696e0 {
type = N11__gnu_debug14_Safe_iteratorIN9__gnu_cxx17__normal_iteratorIPiNSt9__cxx19986vectorIiSaIiEEEEENSt7__debug6vectorIiS6_EEEE (mutable iterator);
state = past-the-end;
references sequence with type `NSt7__debug6vectorIiSaIiEEE' @ 0x0x7fff82869710
}
123
如果我们使用 pointers,无论我们是否处于调试模式,上述情况都不会发生。
如果我们不为 libstdc++ 启用 调试模式 ,一个性能更友好的版本(没有添加簿记)实现将是使用 - 并且不会发出任何诊断。
(可能)更好的类型安全
由于迭代器的实际类型是实现定义的,这可以用来提高类型安全性——但你会检查您的实施文档以了解是否属于这种情况。
考虑以下示例:
#include <vector>
struct A { };
struct B : A { };
// .-- oops
// v
void it_func (std::vector<B>::iterator beg, std::vector<A>::iterator end);
void ptr_func (B * beg, A * end);
// ^-- oops
int
main (int argc, char *argv[])
{
std::vector<B> v1;
it_func (v1.begin (), v1.end ()); // (A)
ptr_func (v1.data (), v1.data () + v1.size ()); // (B)
}
详解
根据实现的不同,- (A) 可能是编译时错误,因为
std::vector<A>::iterator
和 std::vector<B>::iterator
可能不相同类型。
然而,- (B) 总是会编译,因为存在从
B*
到 A*
. 的隐式转换
迭代器旨在提供对指针的抽象。
例如,递增迭代器始终会操纵迭代器,以便如果集合中有下一项,它会引用下一项。如果它已经引用了集合中的最后一项,则在增量之后它将是一个无法取消引用的唯一值,但将与另一个指向同一集合末尾的迭代器进行比较(通常使用 collection.end()
).
在迭代器到字符串(或向量)的特定情况下,指针提供了迭代器所需的所有功能,因此指针可以用作迭代器而不会丢失所需的功能。
例如,您可以使用 std::sort
对字符串或向量中的项目进行排序。由于指针提供了所需的功能,您还可以使用它对本机(C 风格)数组中的项目进行排序。
与此同时,是的,定义(或使用)与指针分离的迭代器可以提供并非严格要求的额外功能。例如,某些迭代器至少提供某种程度的检查,以确保(例如)当您比较两个迭代器时,它们都是同一集合中的迭代器,并且您不会尝试越界访问。原始指针不能(或至少通常不会)提供这种能力。
大部分都归结为 "don't pay for what you don't use" 心态。如果您真的只需要并想要本机指针的功能,则可以将它们用作迭代器,并且您通常会获得与直接操作指针所获得的代码本质上相同的代码。同时,对于确实需要额外功能的情况,例如遍历线程 RB 树或 B+ 树而不是简单数组,迭代器允许您在维护单个简单接口的同时做到这一点。同样,如果您不介意为额外的安全性支付额外费用(就存储 and/or 运行 时间而言),您也可以得到它(并且它与诸如个人算法之类的东西分离,所以你可以在你想要的地方得到它,而不用被迫在其他地方使用它,例如,可能对时序要求过于挑剔而无法支持它。
在我看来,很多人在谈到迭代器时都忽略了重点。许多人高兴地重写了这样的东西:
for (size_t i=0; i<s.size(); i++)
...变成类似这样的东西:
for (std::string::iterator i = s.begin; i != s.end(); i++)
...并表现得好像这是一项重大成就。我不认为是。对于这种情况,用迭代器替换整数类型可能收效甚微(如果有的话)。同样,使用您 post 编写的代码并将 char const *
更改为 std::string::iterator
似乎不太可能完成很多(如果有的话)。事实上,这样的转换通常会使代码更冗长和更难理解,而在 return.
中却一无所获。
如果您要更改代码,您应该(在我看来)这样做,通过使其真正通用(std::string::iterator
确实不会这样做)来尝试使其更加通用).
例如,考虑您的 split
(从您链接的 post 复制):
vector<string> split(const char* start, const char* finish){
const char delimiters[] = ",(";
const char* it;
vector<string> result;
do{
for (it = find_first_of(start, finish, begin(delimiters), end(delimiters));
it != finish && *it == '(';
it = find_first_of(extractParenthesis(it, finish) + 1, finish, begin(delimiters), end(delimiters)));
auto&& temp = interpolate(start, it);
result.insert(result.end(), temp.begin(), temp.end());
start = ++it;
} while (it <= finish);
return result;
}
就目前而言,这仅限于在窄字符串上使用。如果有人想使用宽字符串、UTF-32 字符串等,要做到这一点就相对困难了。同样,如果有人想匹配 [
或 '{' 而不是 (
,也需要为此重写代码。
如果有机会支持各种字符串类型,我们可能想让代码更通用,像这样:
template <class InIt, class OutIt, class charT>
void split(InIt start, InIt finish, charT paren, charT comma, OutIt result) {
typedef std::iterator_traits<OutIt>::value_type o_t;
charT delimiters[] = { comma, paren };
InIt it;
do{
for (it = find_first_of(start, finish, begin(delimiters), end(delimiters));
it != finish && *it == paren;
it = find_first_of(extractParenthesis(it, finish) + 1, finish, begin(delimiters), end(delimiters)));
auto&& temp = interpolate(start, it);
*result++ = o_t{temp.begin(), temp.end()};
start = ++it;
} while (it != finish);
}
这还没有经过测试(甚至编译),所以它实际上只是一个大致方向的草图,您可以使用代码,而不是实际的完成代码。尽管如此,我认为一般的 idea 至少应该是显而易见的——我们不只是将其更改为 "use iterators"。我们将其更改为通用的,迭代器(作为模板参数传递,此处未直接指定类型)只是其中的一部分。为了走得更远,我们还消除了对括号和逗号字符的硬编码。虽然不是绝对必要的,但我也更改了参数以更符合标准算法使用的约定,因此(例如)输出也是通过迭代器编写的,而不是 returned 作为集合。
虽然可能不会立即显现出来,但后者确实增加了相当大的灵活性。举个例子,如果有人只是想在拆分后打印出字符串,他可以传递一个 std::ostream_iterator
,让每个结果在生成时直接写入 std::cout
,而不是获取字符串向量, 然后必须单独打印出来。
对于那些看不到已删除帖子的人,评论是关于我在 const char*
s 而不是 string::const_iterator
s:"Iterators may have been a better path from the get go, since it appears that is exactly how your pointers seems be treated."
所以我的问题是,迭代器是否持有 string::const_iterator
s 持有超过 const char*
s 的任何内在价值,以便将我的答案切换到 string::const_iterators
有意义吗?
简介
使用迭代器代替指针有很多好处,其中包括:
- release 与 debug 中的不同代码路径,以及;
- 更好的类型安全,并且;
- 使编写通用代码成为可能(迭代器可以用于任何数据结构,例如链表,而内部指针在这方面非常有限)。
调试
因为除其他外,取消引用传递到范围末尾的迭代器是 未定义的行为,在这种情况下,实现可以自由地做任何它认为必要的事情- 包括提出诊断说你做错了什么。
标准库实现,libstdc++, provided by gcc will issues diagnostics when it detects something fault (if Debug Mode 已启用)。
例子
#define _GLIBCXX_DEBUG 1 /* enable debug mode */
#include <vector>
#include <iostream>
int
main (int argc, char *argv[])
{
std::vector<int> v1 {1,2,3};
for (auto it = v1.begin (); ; ++it)
std::cout << *it;
}
/usr/include/c++/4.9.2/debug/safe_iterator.h:261:error: attempt to
dereference a past-the-end iterator.
Objects involved in the operation:
iterator "this" @ 0x0x7fff828696e0 {
type = N11__gnu_debug14_Safe_iteratorIN9__gnu_cxx17__normal_iteratorIPiNSt9__cxx19986vectorIiSaIiEEEEENSt7__debug6vectorIiS6_EEEE (mutable iterator);
state = past-the-end;
references sequence with type `NSt7__debug6vectorIiSaIiEEE' @ 0x0x7fff82869710
}
123
如果我们使用 pointers,无论我们是否处于调试模式,上述情况都不会发生。
如果我们不为 libstdc++ 启用 调试模式 ,一个性能更友好的版本(没有添加簿记)实现将是使用 - 并且不会发出任何诊断。
(可能)更好的类型安全
由于迭代器的实际类型是实现定义的,这可以用来提高类型安全性——但你会检查您的实施文档以了解是否属于这种情况。
考虑以下示例:
#include <vector>
struct A { };
struct B : A { };
// .-- oops
// v
void it_func (std::vector<B>::iterator beg, std::vector<A>::iterator end);
void ptr_func (B * beg, A * end);
// ^-- oops
int
main (int argc, char *argv[])
{
std::vector<B> v1;
it_func (v1.begin (), v1.end ()); // (A)
ptr_func (v1.data (), v1.data () + v1.size ()); // (B)
}
详解
-
根据实现的不同,
- (A) 可能是编译时错误,因为
std::vector<A>::iterator
和std::vector<B>::iterator
可能不相同类型。
然而, - (B) 总是会编译,因为存在从
B*
到A*
. 的隐式转换
迭代器旨在提供对指针的抽象。
例如,递增迭代器始终会操纵迭代器,以便如果集合中有下一项,它会引用下一项。如果它已经引用了集合中的最后一项,则在增量之后它将是一个无法取消引用的唯一值,但将与另一个指向同一集合末尾的迭代器进行比较(通常使用 collection.end()
).
在迭代器到字符串(或向量)的特定情况下,指针提供了迭代器所需的所有功能,因此指针可以用作迭代器而不会丢失所需的功能。
例如,您可以使用 std::sort
对字符串或向量中的项目进行排序。由于指针提供了所需的功能,您还可以使用它对本机(C 风格)数组中的项目进行排序。
与此同时,是的,定义(或使用)与指针分离的迭代器可以提供并非严格要求的额外功能。例如,某些迭代器至少提供某种程度的检查,以确保(例如)当您比较两个迭代器时,它们都是同一集合中的迭代器,并且您不会尝试越界访问。原始指针不能(或至少通常不会)提供这种能力。
大部分都归结为 "don't pay for what you don't use" 心态。如果您真的只需要并想要本机指针的功能,则可以将它们用作迭代器,并且您通常会获得与直接操作指针所获得的代码本质上相同的代码。同时,对于确实需要额外功能的情况,例如遍历线程 RB 树或 B+ 树而不是简单数组,迭代器允许您在维护单个简单接口的同时做到这一点。同样,如果您不介意为额外的安全性支付额外费用(就存储 and/or 运行 时间而言),您也可以得到它(并且它与诸如个人算法之类的东西分离,所以你可以在你想要的地方得到它,而不用被迫在其他地方使用它,例如,可能对时序要求过于挑剔而无法支持它。
在我看来,很多人在谈到迭代器时都忽略了重点。许多人高兴地重写了这样的东西:
for (size_t i=0; i<s.size(); i++)
...变成类似这样的东西:
for (std::string::iterator i = s.begin; i != s.end(); i++)
...并表现得好像这是一项重大成就。我不认为是。对于这种情况,用迭代器替换整数类型可能收效甚微(如果有的话)。同样,使用您 post 编写的代码并将 char const *
更改为 std::string::iterator
似乎不太可能完成很多(如果有的话)。事实上,这样的转换通常会使代码更冗长和更难理解,而在 return.
如果您要更改代码,您应该(在我看来)这样做,通过使其真正通用(std::string::iterator
确实不会这样做)来尝试使其更加通用).
例如,考虑您的 split
(从您链接的 post 复制):
vector<string> split(const char* start, const char* finish){
const char delimiters[] = ",(";
const char* it;
vector<string> result;
do{
for (it = find_first_of(start, finish, begin(delimiters), end(delimiters));
it != finish && *it == '(';
it = find_first_of(extractParenthesis(it, finish) + 1, finish, begin(delimiters), end(delimiters)));
auto&& temp = interpolate(start, it);
result.insert(result.end(), temp.begin(), temp.end());
start = ++it;
} while (it <= finish);
return result;
}
就目前而言,这仅限于在窄字符串上使用。如果有人想使用宽字符串、UTF-32 字符串等,要做到这一点就相对困难了。同样,如果有人想匹配 [
或 '{' 而不是 (
,也需要为此重写代码。
如果有机会支持各种字符串类型,我们可能想让代码更通用,像这样:
template <class InIt, class OutIt, class charT>
void split(InIt start, InIt finish, charT paren, charT comma, OutIt result) {
typedef std::iterator_traits<OutIt>::value_type o_t;
charT delimiters[] = { comma, paren };
InIt it;
do{
for (it = find_first_of(start, finish, begin(delimiters), end(delimiters));
it != finish && *it == paren;
it = find_first_of(extractParenthesis(it, finish) + 1, finish, begin(delimiters), end(delimiters)));
auto&& temp = interpolate(start, it);
*result++ = o_t{temp.begin(), temp.end()};
start = ++it;
} while (it != finish);
}
这还没有经过测试(甚至编译),所以它实际上只是一个大致方向的草图,您可以使用代码,而不是实际的完成代码。尽管如此,我认为一般的 idea 至少应该是显而易见的——我们不只是将其更改为 "use iterators"。我们将其更改为通用的,迭代器(作为模板参数传递,此处未直接指定类型)只是其中的一部分。为了走得更远,我们还消除了对括号和逗号字符的硬编码。虽然不是绝对必要的,但我也更改了参数以更符合标准算法使用的约定,因此(例如)输出也是通过迭代器编写的,而不是 returned 作为集合。
虽然可能不会立即显现出来,但后者确实增加了相当大的灵活性。举个例子,如果有人只是想在拆分后打印出字符串,他可以传递一个 std::ostream_iterator
,让每个结果在生成时直接写入 std::cout
,而不是获取字符串向量, 然后必须单独打印出来。