【问题标题】:Is it possible to test whether two iterators point to the same object?是否可以测试两个迭代器是否指向同一个对象?
【发布时间】:2012-07-26 01:53:59
【问题描述】:

假设我正在创建一个函数来复制一个值:

template<class ItInput, class ItOutput>
void copy(ItInput i, ItOutput o) { *o = *i; }

如果io 指向同一个对象,我想避免分配,因为这样分配就没有意义了。

显然,我不能说if (i != o) { ... },因为io 可能属于不同类型,并且因为它们可能指向不同的容器(因此无法比较)。不太明显的是,我也不能使用重载的函数模板,因为迭代器可能属于不同的容器,即使它们具有相同的类型。

我最初的解决方案是:

template<class ItInput, class ItOutput>
void copy(ItInput i, ItOutput o)
{
    if (&*o != static_cast<void const *>(&*i))
        *o = *i;
}

但我不确定这是否有效。如果*o*i 实际上返回一个对象而不是引用呢?

一般有没有办法做到这一点?

【问题讨论】:

  • 哦,我明白了......'aite。喜欢这个:)
  • 当您说“...如果输入和输出相同...”时,您是指相同的值,还是指向容器中相同位置的迭代器?
  • 我同意 w/chrad,您的解决方案很好。您对 *o/*i 返回对象的担忧具有更重要的含义,因为函数 *o = *i; 在这种情况下也不起作用,因为两者属于同一类型。
  • 迭代器需要什么类别的迭代器?它们实际上可以是输入和输出迭代器吗?输入迭代器只是单次传递,因此您不能将其间接两次。 严格来说,只有输入迭代器可以是代理迭代器(operator* 返回对象而不是引用的迭代器)。前向、双向和随机访问迭代器不能返回代理。在实践中,并不总是遵循这一点,因为拥有一个提供随机访问的代理迭代器非常有用(另外,我不知道如何在不这样做的情况下有效地实现vector&lt;bool&gt;
  • @LucDanton:重要的规则在 §24.2.5[forward.iterators]/6 中,它要求两个前向迭代器 ab,“如果 a 和 @987654336 @ 都是可取消引用的,然后 a == b 当且仅当 *a*b 绑定到同一个对象。”只有在间接产生引用时才能满足该要求。

标签: c++ stl iterator


【解决方案1】:

我不认为这真的有必要:如果赋值很昂贵,该类型应该定义一个赋值运算符来执行(相对便宜的)自赋值检查,以防止做不必要的工作。但是,这是一个有趣的问题,有很多陷阱,所以我会尝试回答它。

如果我们要组装一个适用于输入和输出迭代器的通用解决方案,我们必须注意几个陷阱:

  • 输入迭代器是单遍迭代器:每个元素只能通过迭代器执行一次间接,因此,我们不能通过迭代器执行一次间接来获取指向值的地址第二次执行复制。

  • 输入迭代器可能是代理迭代器。代理迭代器是一个迭代器,其operator* 返回一个对象,而不是一个引用。使用代理迭代器,表达式 &amp;*it 格式错误,因为 *it 是一个右值(可能会重载一元 -&amp;,但这样做通常被认为是邪恶和可怕的,大多数类型都不会这样做这个)。

  • 一个输出迭代器只能用于输出;您不能通过它执行间接并将结果用作右值。您可以写入“指向元素”,但不能从中读取。

所以,如果我们要进行“优化”,我们只需要在两个迭代器都是前向迭代器的情况下进行优化(这包括双向迭代器和随机访问迭代器:它们也是前向迭代器)。

因为我们很好,我们还需要注意一个事实,尽管它违反了概念要求,但许多代理迭代器歪曲了他们的类别,因为拥有一个支持随机访问的代理迭代器非常有用在一系列代理对象上。 (我什至不确定如何在不这样做的情况下为std::vector&lt;bool&gt; 实现高效的迭代器。)

我们将使用以下标准库标头:

#include <iterator>
#include <type_traits>
#include <utility>

我们定义了一个元函数is_forward_iterator,它测试一个类型是否是“真正的”前向迭代器(即,不是代理迭代器):

template <typename T>
struct is_forward_iterator :
    std::integral_constant<
        bool,
        std::is_base_of<
            std::forward_iterator_tag,
            typename std::iterator_traits<T>::iterator_category
        >::value &&
        std::is_lvalue_reference<
            decltype(*std::declval<T>())
        >::value>
{ };

为简洁起见,我们还定义了一个元函数can_compare,用于测试两种类型是否都是前向迭代器:

template <typename T, typename U>
struct can_compare :
    std::integral_constant<
        bool,
        is_forward_iterator<T>::value &&
        is_forward_iterator<U>::value
    >
{ };

然后,我们将编写 copy 函数的两个重载,并使用 SFINAE 根据迭代器类型选择正确的重载:如果两个迭代器都是前向迭代器,我们将包含检查,否则我们将排除检查并始终执行分配:

template <typename InputIt, typename OutputIt>
auto copy(InputIt const in, OutputIt const out)
    -> typename std::enable_if<can_compare<InputIt, OutputIt>::value>::type
{
    if (static_cast<void const volatile*>(std::addressof(*in)) != 
        static_cast<void const volatile*>(std::addressof(*out)))
        *out = *in;
}

template <typename InputIt, typename OutputIt>
auto copy(InputIt const in, OutputIt const out)
    -> typename std::enable_if<!can_compare<InputIt, OutputIt>::value>::type
{
    *out = *in;
}

像馅饼一样简单!

【讨论】:

  • 对不起,延迟评论,但我只是在做了一堆研究后才找到这个答案(包括一篇关于 comp.std.c++ 的帖子,其中有一个有趣的非代理迭代器案例类似的问题)。两个前向迭代器,即使是相同类型的,除非它们具有相同的底层序列 (24.2.5/2),否则它们是不可比较的。所以除非你知道两个迭代器在一个共同的范围内,否则比较它们是UB,我不相信有任何方法可以测试迭代器是否在同一范围内,因为24.2.1/6中的测试依赖于在==.
  • @rici:你反对这个答案的哪一部分?
  • 啊,对不起。我误读了 can_compare (将其与其名称混淆)。所以你的意图是绕过operator==,因为两个地址比较相等这一事实足以证明指向的对象本质上是同一个对象,即使对象是不同的类型。这也让我觉得有问题,但这不是我最初的问题,所以我撤回了我的反对意见,并对噪音感到抱歉。
【解决方案2】:

我认为在这种情况下,您可能必须记录一些关于您期望在函数中使用的类型的假设,并且满足于不完全通用。

operator*operator&amp; 一样可以重载以执行各种操作。如果你防operator*,那么你应该考虑operator&amp;operator!=等。

我想说强制执行的一个很好的先决条件(通过代码中的 cmets 或概念/静态断言)是 operator* 返回对迭代器指向的对象的引用,并且它不(或不应该) 't) 执行副本。在这种情况下,您的代码看起来还不错。

【讨论】:

  • 我应该忽略std::iterator_traits&lt;It&gt;::referencestd::iterator_traits&lt;It&gt;::pointer吗?我的意思是,如果我可以将迭代器解引用为普通引用和(以及它们的地址作为指针),那么在iterator_traits 中定义这些类型定义中的任何一个都没有意义,对吧? value_type 就足够了。
  • @Mehrdad 定义这些是因为它们使使用具有较少琐碎功能的迭代器变得容易得多。有时您需要使用迭代器指向的实际类型,然后您需要使用这些 typedef。
  • “有时你需要使用迭代器指向的实际类型” 如果我可以将迭代器视为指针,那不就是value_type吗?那我为什么需要pointerreference
  • @Mehrdad 您不能将迭代器视为指针。迭代器概念不能保证&amp;*it 是正确的(或者它的类型应该是什么),所以你必须遵循这里的建议并推出你自己的概念来解释模板期望&amp;*it != &amp;*it 工作。这为潜在用户提供了一个机会,例如如果需要,将重载的 operator&amp; 提供或修复为 reference 类型。
  • "我会说一个很好的假设是 operator* 返回对迭代器指向的对象的引用" -- 除非它不是,因为输入迭代器可以取消引用任何“可转换到T" (§24.2.3/2)。
【解决方案3】:

按原样,您的代码肯定好,或者至少对于所有迭代器类别都不好。

输入迭代器和输出迭代器在第一次之后不需要被取消引用(它们应该是单次传递),并且输入迭代器可以取消引用任何“可转换为T”(§24.2.3 /2)。

所以,如果你想处理各种迭代器,我认为你不能强制执行这种“优化”,即你不能一般地检查两个迭代器是否指向同一个对象。如果您愿意放弃输入和输出迭代器,那么您所拥有的应该没问题。否则,无论如何我都会坚持做副本(我真的认为您对此没有其他选择)。

【讨论】:

  • 我猜我猜代码还有其他问题,所以我并不感到惊讶它不正常......我要问的是,有没有办法实现我的要求? (如果正确,“否”是可以接受的答案...)
【解决方案4】:

编写一个帮助模板函数equals,如果迭代器类型不同,它会自动返回false。要么这样做,要么对您的 copy 函数本身进行专门化或重载。

如果它们是同一类型,那么您可以使用比较它们解析到的对象的指针的技巧,无需强制转换:

if (&*i != &*o)
    *o = *i;

如果 *i*o 没有返回引用,没问题 - 即使不需要,也会复制,但不会造成任何伤害。

【讨论】:

  • @Mehrdad,你能说得更具体点吗?哪个部分不起作用,为什么?
  • “我也不能使用重载的模板函数,因为迭代器可能属于不同的容器,即使它们具有相同的类型。”
  • @Mehrdad,通过取消引用迭代器,它们是否属于同一个容器不再重要。除非你认为两个容器的元素可能具有相同的指针地址,但我认为这是不可能的。
  • 这很重要,因为我认为该标准不允许您比较属于不同容器的迭代器。
  • @Mehrdad,您不是在比较迭代器 - 您是在比较迭代器指向的对象。既然这是你的主意,我很惊讶你没有意识到。
猜你喜欢
  • 2013-07-03
  • 1970-01-01
  • 2011-03-31
  • 2021-11-05
  • 2012-08-22
  • 2012-11-28
  • 2012-08-23
  • 1970-01-01
  • 2012-06-30
相关资源
最近更新 更多