接口与协方差问题

Interfaces and covariance problem

本文关键字:问题 方差 接口      更新时间:2023-10-16

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

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. 包含纯虚成员函数,这些函数返回*retvalx成员的引用,retval->返回x的地址,++retval使retval指向数组中的下一个Something
  2. 纯虚成员返回的东西可以继承,并由成员
  3. 的实现返回。
  4. container[i](其中container是保存Something对象的数组)总是返回一些东西,使得*retval总是返回对相同i的相同T的引用。

现在,界面看起来是这样的:

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&)与要求没有问题,除了用于++retval要求。因为Something直接绑定到它所持有的对象,不能用指针指向T,所以我无法找到让++使变量指向数组中的下一个Something的方法。

如果有帮助的话,这是一个迭代器类型系统。我本可以用STL风格的迭代器(你只是有一个T数组),通过值传递,并保持指针指向它们所代表的值,但这会破坏接口,因为只有引用和指针是协变的,对象已经存在于其他地方了(在我的代码中,它们在数组中),所以你不返回对本地对象的引用。

这样设置的目的是为了编写接受Container&并迭代容器的函数,而不知道它是什么类型的容器:

void iterate(Container<int>& somecontainer) {
    Container<int>::IIterator i = somecontainer.begin(); // right now this would return a reference, but it doesn't/can't work that way
    while (i != somecontainer.end()) {
         doSomething(*i);
         ++i; // this is the problem
    }
}
这对我来说很难描述,如果你需要更多的信息,请不要犹豫,告诉我。

您要做的事情叫做类型擦除。基本上,您需要提供一个值类型(在整个继承层次结构中都是一样的),它包装了特定的迭代器类型,并提供了一个统一的动态接口。

类型擦除通常使用非虚类(类型擦除)实现,该类存储指向实现擦除的虚基类的指针,从中派生出包装每个特定迭代器的不同类型。静态类将提供模板化的构造函数/赋值操作符,这些操作符将动态地实例化派生类型的对象并在内部存储指针。然后,您只需要将操作集作为分派实现到内部对象。

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

素描:

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类型的限制)

用于扩展阅读:论c++中面向对象编程与泛型编程的矛盾- any_iterator:为c++迭代器实现Erasure- adobe any_iterator,

我建议你看看Visitor模式。

除此之外,你想要的是一个将被注入多态行为的值类型。有一个比James使用你的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实现是无状态的,并且实现"过滤"迭代器,例如,将变得不可能。

您考虑过使用CRTP吗?我觉得它在这里很合适。下面是一个简短的演示。它只是解释了你的++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);
}

然而,对于你的问题,它可能是一个很好的选择。

就像你说的,问题是Something的实例被绑定到它所持有的对象。所以让我们试着解开它们。

要记住的关键一点是,在OOP中,公共非const数据成员通常是不受欢迎的。在当前的实现中,每个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数据抽象原则中最重要的一点。

编辑

至于实现Containerbeginend方法,这应该也不是太困难,但它需要对Container进行一些更改。

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

类Something: IInterface{

    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迭代器(这可以保护我们在用户给出错误时在随机内存上迭代)。

接下来,我们稍微改变一下MyContainer,这样我们就不会有一个Something的数组,而是一个T的数组:

class MyContainer
{
    ...
private:
    T *data;
};

在我们实现beginend之前,让我们对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方法现在很容易:

template<class T>
MyContainer<T>::begin()
{
    return Something(data);
}
template<class T>
MyContainer<T>::end()
{
    // this part depends on your implementation of MyContainer.
    // I'll just assume your have a length field in MyContainer.
    return Something(data + length);
}

这就是我午夜的想法。就像我上面提到的,我目前无法测试这段代码,所以您可能需要稍微调整一下。希望这能满足你的要求。

如果使用方法与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.
};