如何使我的迭代器类不像容器类?

9

前提条件

假设我有一个容器类Box,它提供了内部类const_iteratoriterator。因为我希望iterator可以转换为const_iterator,所以后者继承自前者:

class Box {
  // ...
public:
  class const_iterator : public std::iterator<std::random_access_iterator_tag, const int> { /* ... */ };
  class iterator : public const_iterator { /* ... */ };
  // ...
};

问题

现在我想使用Google Test来测试这些类。让我们断言begin()end()不返回相同的东西:

const Box a;
EXPECT_NE(a.begin(), a.end());

遇到编译错误:

  • clang: Box::const_iterator中没有名为'begin'的成员
  • g++: 'const class Box::const_iterator'没有名为'begin'的成员

原因

查阅资料后,我找到了Google Test源代码中的这个模板(点击链接获取详细文档)。

typedef int IsContainer;
template <class C>
IsContainer IsContainerTest(int /* dummy */,
                            typename C::iterator* /* it */ = NULL,
                            typename C::const_iterator* /* const_it */ = NULL) {
  return 0;
}

这个模板的神奇之处在于,如果EXPECT_*的参数具有iteratorconst_iterator成员类,则假定该类型是一个容器类。Google Test可以利用这一点,在期望失败时打印出相当人性化的报告,这很好。但是,还有一个小细节:
// Note that we look for both C::iterator and C::const_iterator.  The
// reason is that C++ injects the name of a class as a member of the
// class itself (e.g. you can refer to class iterator as either
// 'iterator' or 'iterator::iterator').  If we look for C::iterator
// only, for example, we would mistakenly think that a class named
// iterator is an STL container.

如果我理解正确的话,这意味着:
- `Box::const_iterator` 作为一个成员类被命名为 `const_iterator`,而 `std::iterator` 作为一个成员类被命名为 `iterator`。 - `Box::iterator` 作为一个成员类被命名为 `iterator`,而 `Box::const_iterator` 作为一个成员类被命名为 `const_iterator`。
因此,我的两个迭代器类都看起来像容器类,对于 Google Test 来说!
问题是:如何设计我的迭代器类,使它们不再看起来像容器?
我尝试过的方法有:
- 将 `const_iterator` 的 `std::iterator` 超类声明为 `private`。这样可以通过隐藏 `iterator` 成员类来解决 `const_iterator` 的问题,但是除非 `a` 是 `const`,否则它仍然不能让我将 `a.begin()` 作为参数传递给 `EXPECT_NE`。看起来 Google Test 使用的是 `iterator begin()` 而不是 `const_iterator begin() const`,原因不明。 - 完全删除 `std::iterator` 超类。这是一个坏主意吗?我想我必须手动声明我的 `std::iterator_traits`,还有其他我不扩展 `std::iterator` 就会失去的东西吗? - 将 `Box::const_iterator` 超类声明为 `private`。这可能是一个选项,因为我需要重新声明我宁愿重用的方法(如 `operator++`)。
还有其他我忽略的东西吗?
#include<iterator>
#include <memory> //unique_ptr<T>
#include <gtest/gtest.h>

class ThreeInts {
  std::unique_ptr<int[]> v;

  public:
  ThreeInts() : v(new int[3]) { v[0] = 0; v[1] = 1; v[2] = 2; };
  ThreeInts(int val) : ThreeInts() { v[0] = val; v[1] = val; v[2] = val; };

  bool operator==(const ThreeInts& other) const {
    return v[0] == other.v[0] && v[1] == other.v[1] && v[2] == other.v[2];
  }

  class const_iterator : public std::iterator<std::random_access_iterator_tag, const int> {
  protected:
    int* p;
  public:
    explicit const_iterator(int* p) : p(p) {}
    const_iterator& operator++() { ++p; return *this; }
    bool operator==(const const_iterator& rhs) const { return p == rhs.p; }
    bool operator!=(const const_iterator& rhs) const { return p != rhs.p; }
    int operator*() const { return *p; }
  };

  class iterator : public const_iterator {
  public:
    explicit iterator(int* p) : const_iterator(p) {}
    int& operator*() const { return *p; }
  };

  iterator begin() { return iterator(v.get()); }
  iterator end() { return iterator(v.get()+3); }
  const_iterator begin() const { return const_iterator(v.get()); }
  const_iterator end() const { return const_iterator(v.get()+3); }
};

TEST(ThreeInts, ThisTestCompilesAndPrettyFailureMessagesAreShown) {
  const ThreeInts a(1), b(2);
  ThreeInts c(1), d(2);
  EXPECT_EQ(a, b);
  EXPECT_EQ(a, c);
  EXPECT_EQ(c, d);
}

TEST(ThreeInts, ThisTestCompilesIfTheStdIteratorParentIsPrivate) {
  const ThreeInts a;
  EXPECT_NE(a.begin(), a.end());
}

TEST(ThreeInts, ThisTestAlsoCompilesIfTheStdIteratorParentIsPrivateButItIsAHassle) {
  ThreeInts a;
  ThreeInts::const_iterator beg = a.begin();
  ThreeInts::const_iterator end = a.end();
  //EXPECT_NE(beg, end); // Compile error unless the std::iterator superclass is private
}

TEST(ThreeInts, ThisTestDoesNotCompileEvenIfTheStdIteratorParentIsPrivate) {
  ThreeInts a;
  //EXPECT_NE(a.begin(), a.end());
}

int main(int argc, char **argv) {
  ::testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}

2
将您的迭代器类命名为ThreeIntsIterator或其他名称,但不要使用iterator。在ThreeInts中,执行typedef ThreeIntsIterator iterator; - Igor Tandetnik
Google测试中的IsContainerTest是垃圾 - 简单的typedef不构成容器 - 更好的方法是测试成员函数begin()end(),但这种方法也有缺点。 - user2249683
你没有正确理解那个注释。它查找C::iteratorC::const_iterator,因为如果只查找前者,那么C可能只是一个具有子类型iterator而不是容器。 - Walter
1
我建议对Google Test提出错误报告。显然,EXPECT_NE不应该在没有证明其存在的情况下调用其参数的begin()end()函数。即使是这样,也值得怀疑。这种模板魔法是耗时微妙的错误的绝佳来源 - Google的人们应该意识到他们的代码试图做的荒谬之处。 - cmaster - reinstate monica
1
iterator继承const_iterator方面存在一些问题。例如考虑operator++,你想要重用它,但是它返回什么?是的,在两种情况下都返回const_iterator&。这是不正确的,并且可能会破坏标准算法。对于你想要实现的+-也是如此。最好分别实现两个迭代器类(可能使用CRTP从一个共同的父类继承),并提供转换运算符。 - n. m.
显示剩余7条评论
1个回答

1

ThreeInts::iterator不应该继承自ThreeInts::const_iterator,而是应该分别实现它们。

class ThreeInts::iterator : public std::iterator< std::random_access_iterator_tag, int> { ... }
class ThreeInts::const_iterator : public std::iterator< std::random_access_iterator_tag, const int> { ... }

问题似乎在于,否则ThreeInts :: const_iterator既有名为const_iterator的成员,也有名为iterator(即构造函数)的成员。此外,使iterator继承自const_iterator不是符合常量正确性的,因为const_iterator应仅保留指向const数据的指针/类似指针。STL容器还将这两个迭代器分开。
在该代码中,可能只需定义简单的iterator类,而不是定义迭代器类。
using iterator = int*;
using const_iterator = const int*;

你说得对,const_iterator 只应该持有 const 数据,这个细节我忘了。但是我能不能让 iterator 继承自 const_iterator 并且只是用非 const 数据覆盖 const 数据呢?等我有时间了我会试一下的。还要感谢你建议将它们分开。现在,一个更正:gtest 中使用的魔法模板明确指定了 typename,所以问题在于类作为成员嵌入,而不是构造函数。另外,请不要被我的 ThreeInts 示例的实现细节所分心。 - Emil Lundberg

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