接口和协方差问题

时间:2011-08-08 04:41:15

标签: c++ interface iterator covariance

我有一个特定的类来存储一段数据,它实现了一个接口:

template<typename T>
class MyContainer : public Container<T> {
    class Something : public IInterface {
    public:
        // implement *, ->, and ++ here but how?
    private:
        T x;
    };

    // implement begin and end here, but how?

private:
    Something* data; // data holds the array of Somethings so that references to them can be returned from begin() and end() to items in this array so the interface will work, but this makes the problem described below
};

我有一组Something s。

我需要Something来实现接口类(示例中为IInterface):

  1. 包含纯虚拟成员函数,这些函数返回的内容使*retval返回对x成员的引用,retval->返回x的地址,++retval使retval引用数组中的下一个Something
  2. 纯虚拟成员返回的内容可以从成员的实现继承并返回
  3. container[i](其中container是包含Something个对象的数组)始终返回一些内容,*retval始终返回对同一T的引用相同的i
  4. 现在,界面如下所示:

    template<typename T>
    class Container {
        class IInterface {
        public:
            virtual T& operator*() = 0;
            virtual T* operator->() = 0;
            virtual IInterface& operator++(); // this is the problem 
        };
    
        // returning a reference right now to support covariance, so subclasses can
        // derive from Container and then have a member class derive from IInterface
        // and override these to return their derived class, but this has a problem
        virtual IInterface& begin() = 0;
        virtual IInterface& end() = 0;
    };
    

    我当前的解决方案(让虚拟方法返回IInterface&并在实现中返回Something&)的要求没有问题,除了 {{1要求。因为++retval直接绑定到它所拥有的对象,并且不能指向带有指针的Something,所以我无法找到让T使变量引用到阵列中的下一个++

    如果有必要知道,这是一个迭代器类型系统。我会用STL样式迭代器(你只有一个Something数组)来实现它,这些迭代器按值传递,并保存指向它们所代表的值的指针,但这会破坏接口,因为只有引用和指针是协变的,对象已经必须存在于其他地方(在我的代码中它们在数组中)所以你不会返回对本地对象的引用。

    此设置的目的是让人们可以编写带T的函数并迭代容器而不知道它是什么类型的容器:

    Container&

    我很难描述,如果您需要更多信息,请随时告诉我。

5 个答案:

答案 0 :(得分:7)

您尝试执行的操作称为类型擦除。基本上,您希望提供一个值类型(在整个继承层次结构中是相同的),它包装特定的迭代器类型并提供统一的动态接口。

类型擦除通常使用非虚拟类(类型擦除)实现,该类存储指向实现擦除的虚拟基类的指针,派生包装每个特定迭代器的不同类型。静态类将提供模板化的构造函数/赋值运算符,它们动态实例化派生类型的对象并在内部存储指针。然后,您只需要将一组操作实现为对内部对象的调度。

对于最简单的类型擦除形式,您可以查看boost::any的实现(文档是here

草图:

namespace detail {
   template<typename T>
   struct any_iterator_base {
      virtual T* operator->() = 0;    // Correct implementation of operator-> is tough!
      virtual T& operator*() = 0;
      virtual any_iterator_base& operator++() = 0;
   };
   template <typename T, typename Iterator>
   class any_iterator_impl : any_iterator_base {
      Iterator it;
   public:
      any_iterator_impl( Iterator it ) : it(it) {}
      virtual T& operator*() {
         return *it;
      }
      any_iterator_impl& operator++() {
         ++it;
         return *this;
      }
   };
}
template <typename T>
class any_iterator {
   detail::any_iterator_base<T>* it;
public:
   template <typename Iterator>
   any_iterator( Iterator it ) : it( new detail::any_iterator_impl<T,Iterator>(it) ) {}
   ~any_iterator() {
      delete it;
   }
   // implement other constructors, including copy construction
   // implement assignment!!! (Rule of the Three)
   T& operator*() {
      return *it;   // virtual dispatch
   }
};

实际的实施变得非常混乱。您需要为标准中的不同迭代器类型提供不同版本的迭代器,并且操作符的实现细节也许并不简单。特别是operator->迭代应用,直到获得原始指针,并且您希望确保您的类型擦除行为不会破坏该不变量或记录您如何破坏它(即T类型的限制您的适配器可以包装)

扩展阅读:   - On the Tension Between Object-Oriented and Generic Programming in C++   - any_iterator: Implementing Erasure for C++ iterators   - adobe any_iterator

答案 1 :(得分:2)

我建议看一下Visitor模式。

除此之外,你想要的是一个充满多态行为的值类型。有一个比詹姆斯使用你的IInterface更简单的解决方案。

class IInterface
{
  virtual ~IInterface() {}
  virtual void next() = 0;
  virtual void previous() = 0;
  virtual T* pointer() const = 0;

  virtual std::unique_ptr<IInterface> clone() const = 0;
};

std::unique_ptr<IInterface> clone(std::unique_ptr<IInterface> const& rhs) {
  if (!rhs) { return std::unique_ptr<IInterface>(); }
  return rhs->clone();
}

class Iterator
{
  friend class Container;
public:
  Iterator(): _impl() {}

  // Implement deep copy
  Iterator(Iterator const& rhs): _impl(clone(rhs._impl)) {}
  Iterator& operator=(Iterator rhs) { swap(*this, rhs); return *this; }

  friend void swap(Iterator& lhs, Iterator& rhs) {
    swap(lhs._impl, rhs._impl);
  }

  Iterator& operator++() { assert(_impl); _impl->next(); return *this; }
  Iterator& operator--() { assert(_impl); _impl->previous(); return *this; }
  Iterator operator++(int); // usual
  Iterator operator--(int); // usual

  T* operator->() const { assert(_impl); return _impl->pointer(); }
  T& operator*() const { assert(_impl); return *_impl->pointer(); }

private:
  Iterator(std::unique_ptr<IInterface> impl): _impl(impl) {}
  std::unique_ptr<IInterface> _impl;
};

最后,Container课程将提议:

protected:
  virtual std::unique_ptr<IInterface> make_begin() = 0;
  virtual std::unique_ptr<IInterface> make_end() = 0;

并实施:

public:
  Iterator begin() { return Iterator(make_begin()); }
  Iteraotr end() { return Iterator(make_end()); }

注意:

如果您可以避免所有权问题,则可以取消std::unique_ptr。如果您可以将IInterface仅限于行为(通过将状态提取到Iterator),那么您可以使用Strategy模式启动,并使用指针静态分配的对象。这样就可以避免动态分配内存。

当然,这意味着你的迭代器不会那么丰富,因为它需要IInterface实现无状态,例如,实现“过滤”迭代器将变得不可能。

答案 2 :(得分:1)

就像你说的那样,问题是Something的实例与它所拥有的对象相关联。所以我们试着解开它们。

要记住的关键点是,在OOP中,公共非常量数据成员通常不赞成。在您当前的实现中,每个Something实例都与拥有可公开访问的数据成员T x相关联。而不是这样,被认为更好地对此进行抽象,即改为提供访问器方法:

class Something : IInterface
{
private:
    T x;

public:
    T GetX()
    {
        return x;
    }
};

现在,用户已经知道x是什么类型的事物,更不用说x存在。

这是一个很好的第一步,但是,因为你希望能够x在不同的时间引用不同的对象,我们几乎必须让x成为一个指针。作为对传统代码的让步,我们还会使GetX()返回一个const引用,而不是常规值:

class Something: IInterface
{
private:
    T *x;

public:
    T const& GetX()
    {
        return *x;
    }
};

现在实现IInterface中的方法是微不足道的:

class Something: IInterface
{
private:
   T *x;

public:
    T const& GetX()
    {
        return *x;
    }

    T& operator*()
    {
        return *x;
    }

    T* operator->()
    {
        return x;
    }

    Something& operator++()
    {
        ++x;
        return *this;
    }
};

++运算符现在很简单 - 它实际上只是将++应用于x

用户现在不知道使用了指针。他们所知道的是他们的代码是正确的。这是OOP数据抽象原则中最重要的一点。

修改

就实施begin的{​​{1}}和end方法而言,这也不应该太难,但需要对Container进行一些更改。< / p>

首先,让我们为Container添加一个私有构造函数,它接受一个指向起始对象的指针。我们还会让Something成为MyContainer的朋友:

class Something:IInterface     {

Something

通过使构造函数成为私有,并设置朋友依赖性,我们确保 MyContainer可以创建新的 friend class MyContainer; // Can't test the code right now - may need to be MyContainer<T> or ::MyContainer<T> or something. private: T *x; Something( T * first ) : x(first) { } public: T const& GetX() { return *x; } T& operator*() { return *x; } T* operator->() { return x; } Something& operator++() { ++x; return *this; } }; 迭代器(这可以保护我们迭代随机内存,如果出现错误的话用户)。

接下来,我们将稍微改变一下MyContainer,以便我们只有一个Something数组,而不是Something数组:

T

在我们开始实施class MyContainer { ... private: T *data; }; begin之前,让我们更改为end我谈到过:

Container

因此,我们不是依赖协方差(在这种情况下真的很难),而是使用一点模板魔法来做我们想要的事情。

当然,由于Container现在接受另一个类型参数,我们需要对template<typename T, typename IteratorType> class Container { public: ... // These prototype are the key. Notice the return type is IteratorType (value, not reference) virtual IteratorType begin() = 0; virtual IteratorType end() = 0; }; 进行相应的更改;即我们需要提供MyContainer作为Something的类型参数:

Container

template<class T> class MyContainer : Container<T, Something> ... / begin方法现在很简单:

end

所以这就是我午夜思考的结果。就像我上面提到的,我目前无法测试此代码,因此您可能需要稍微调整一下。希望这可以做你想要的。

答案 3 :(得分:1)

您是否考虑过使用CRTP。我发现这是一个很好的候选人。这是一个简短的demo。它只是解释了您的++retval问题(如果我理解正确的话)。您必须将IInterface定义从pure virtual更改为CRTP类型界面。

template<class Derived>
struct IInterface
{
  Derived& operator ++ ()
  {
    return ++ *(static_cast<Derived*>(this));
  }
};

struct Something : public IInterface<Something>
{
  int x;
  Something& operator ++ ()
  {
    ++x;
    return *this;
  }
};

CRTP存在一些限制,template将始终遵循您的IInterface。这意味着如果您将Something对象传递给这样的函数:

foo(new Something);

然后,foo()应定义为:

template<typename T>
void foo(IInterface<T> *p)
{
  //...
  ++(*p);
}

但是对于你的问题,它可能是一个不错的选择。

答案 4 :(得分:0)

如果用法应该与stdlib类似,那么迭代器需要是一个值对象,因为它通常会被值大量复制。 (另外,beginend方法返回对?的引用是什么?)

template <class T>
class Iterator
{
    shared_ptr<IIterator> it;
public:
    Iterator(shared_ptr<IIterator>);
    T& operator*() { it->deref(); }
    T* operator->() { return &it->deref(); }
    Iterator& operator++() { it->inc(); return *this; }
    etc.
};