【问题标题】:cbegin, cend invalid results for empty containers in Qt 5.2.1cbegin,cend 为 Qt 5.2.1 中的空容器提供无效结果
【发布时间】:2014-10-20 14:49:06
【问题描述】:

在Qt 5.2.1中,下面的代码结果怎么不一样?

QVector<int> c;
if (c.cbegin() != c.begin())
{
   std::cout << "Argh!" << std::endl;
}

这会打印“argh”,但以下不会。

QVector<int> c;
if (c.begin() != c.cbegin())
{
    std::cout << "Argh!" << std::endl;
}

请注意,cbegin 和 begin 位置是交换的。 但是,如果您更改容器状态,我的意思是例如 push_back 在其中的某些内容,它可以正常工作。 在我看来,在容器上调用任何可变方法之前,cbegin 和 cend 都是无效的。 是错误还是功能?

【问题讨论】:

  • 嗯,总是有std::vector;)
  • 这怎么可能是一个功能
  • 我追踪到 begin 和 cbegin。 cbegin 方法只是返回指向内部缓冲区的指针,begin 方法首先调用 detach 方法(按需写入)。在这种情况下(默认构造)分离调用 reallocData 反过来改变内部缓冲区指针并开始返回这个新指针。我认为这是一个错误,应该报告。
  • @GreenScape 不,就标准库容器而言,comparison is well defined。我对 Qt 了解不多,但考虑到它们记录了 STL 风格的迭代器 是普通指针的 typedef,就语言而言,比较本身是很好定义的。结果应该也符合预期,但它失败的事实是一个不幸的实现细节。
  • @GreenScape 不,标准库容器保证“在表达式 i == j (等)中,ij 表示容器的 iterator 类型的对象,其中一个或两个可以被容器的 const_iterator 类型的对象替换,该对象引用相同的元素,语义没有变化”。

标签: c++ qt c++11


【解决方案1】:

您使用的测试代码与bug report which was filed in 2012 非常相似。它被关闭为无效,因为

constBegin 和 begin 不应该比较。曾经。这是不正确的 完全使用(并且可以通过严格的迭代器检查来捕获),所以 这里没有什么可以解决的。

这是真的。 但函数begin()overloaded

 QVector<T>::iterator       QVector::begin();
 QVector<T>::const_iterator QVector::begin() const;

这是一个未指定的行为,因为 C++ == 运算符的 order of evaluation of the operands 未指定。 C++ 中没有从左到右或从右到左求值的概念。

因此,根据编译器和优化,您最终会得到 iterator 版本的 begin 或 const_iterator 版本。

【讨论】:

  • +1 用于查找错误报告。在正常使用情况下,begin() 的两个重载并不是真正的问题,因为您必须强制转换其中一个操作数才能在同一比较中调用两个不同的重载。如果你这样做了,结果将是未指定的,而不是未定义的,与操作数的评估顺序相同。
  • 等一下,UB在哪里?
  • @T.C.未指定,已修复。
  • 决定你是否会得到iteratorconst_iterator 版本的是它被调用的对象。如果是const,那么你会得到const_iterator,否则你会得到iterator
【解决方案2】:

您观察到的行为与对QVector::beginQVector::cbegin 的调用顺序有关。如果对前者的调用发生在对后者的调用之前,则它们的返回值比较相等,但如果您颠倒顺序,它们的比较不相等。这可以通过以下代码来证明:

QVector<int> c;
std::cout << static_cast<void const *>(c.begin()) << std::endl;
std::cout << static_cast<void const *>(c.cbegin()) << std::endl;

打印的地址将相同。但是,如果你交换两个打印语句的顺序,地址就会不同。

如果你深入研究这两个成员函数的源代码,你会看到

inline iterator begin() { detach(); return d->begin(); }
inline const_iterator cbegin() const { return d->constBegin(); }

进一步追溯,d-&gt;begin()d-&gt;constBegin() 的主体是相同的。所以区别在于第一种情况下对QVector::detach() 的调用。这被定义为

template <typename T>
void QVector<T>::detach()
{
    if (!isDetached()) {
#if QT_SUPPORTS(UNSHARABLE_CONTAINERS)
        if (!d->alloc)
            d = Data::unsharableEmpty();
        else
#endif
            reallocData(d->size, int(d->alloc));
    }
    Q_ASSERT(isDetached());
}

可以在here 找到对正在发生的事情的解释。

Qt 的容器是隐式共享的——当你复制一个对象时,只复制一个指向数据的指针。当对象被修改时,它首先创建数据的深层副本,这样它就不会影响其他对象。创建当天的深层副本的过程称为分离

因为,STL 风格的迭代器在概念上只是指针,它们直接修改底层数据。因此,非常量迭代器在使用 Container::begin() 创建时会分离,因此其他隐式共享实例不会受到更改的影响。

因此,如果首先调用QVector::begin(),则容器的底层存储会被重新分配,随后对QVector::cbegin() 的调用将返回相同的指针。但是,将它们反转,对 QVector::cbegin() 的调用会在任何重新分配发生之前返回一个指向共享存储的指针。

【讨论】:

  • 嗯...但对象开始分离。为什么您给出的示例会根据您创建迭代器的顺序产生不同的指针?
  • @Vitali 似乎对象并没有从分离开始,这很奇怪。如果我在调用begin()/cbegin() 之前添加std::cout &lt;&lt; c.isDetached() &lt;&lt; std::endl;,它将打印0
  • 我可能是错的,但原来的帖子是在未定义的行为领域。两个函数的求值顺序是未定义的。它碰巧在他的编译器和机器组合上以它的方式工作,但它不一定在其他任何地方。我在 Qt 源代码中找到了该位置并了解为什么现在共享它。这似乎是一个愚蠢的设计。默认构造使用静态和共享的哨兵对象。
  • 这绝对是一个错误:Q_REFCOUNT_INITIALIZE_STATIC 是表示 null 的共享 QArrayData 的引用计数应该使用的值。
  • 没关系,这不是错误。我提交了 QTBUG-45490,但后来我意识到行为是正确的。考虑您尝试使用此迭代器插入元素的情况 - 如果它没有分离,那么您将插入到共享数据结构中。解决这个问题的唯一方法是在整个过程中添加 if(d) 检查以处理默认情况,这可能会损害正常情况下的性能,这就是它设计这种方式的原因。
猜你喜欢
  • 1970-01-01
  • 2021-03-30
  • 2020-01-15
  • 1970-01-01
  • 2020-10-26
  • 1970-01-01
  • 2018-10-17
  • 2018-05-27
  • 1970-01-01
相关资源
最近更新 更多