【问题标题】:Interfaces and covariance problem接口和协方差问题
【发布时间】:2011-10-22 02:41:30
【问题描述】:

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

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
};

我有一个Somethings 数组。

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

  1. 包含纯虚成员函数,这些函数返回的内容使得*retval 返回对x 成员的引用,retval-&gt; 返回x 的地址,而++retval 使retval 指向下一个@ 987654331@ 在数组中。
  2. 纯虚成员返回的东西可以从成员的实现继承和返回
  3. container[i](其中container 是包含Something 对象的数组)总是返回这样的东西,*retval 总是返回对相同T 的引用,用于相同的i

现在,界面是这样的:

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

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

这种设置的目的是让人们可以编写函数来接受Container&amp; 并在不知道它是什么类型的容器的情况下迭代容器:

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
    }
}

这对我来说有点难以描述,如果您需要更多信息,请随时告诉我。

【问题讨论】:

  • +1,你也可以提供一些关于IInterface的代码,以及与++retval相关的方法在类中的样子。
  • @iammilind,我很乐意提供我能提供的任何信息,但是“与++retval 相关的方法在课堂上的表现如何”是什么?看看我的更新是不是你的意思。
  • @Cat 这样人们就可以在不知道容器类型的情况下编写采用Container&amp; 的函数,并调用container.begin() 来获取IInterface&amp; 并遍历容器的元素.
  • T 有什么限制? T 可以是任何类型吗?如果是这样,您将不得不使用类似boost::any 的东西来破解它,在这种情况下,该类的用户需要知道*container.begin() 的类型是什么。不过,听起来您确实想要一个模板。
  • @Nicol:我不同意忽略差异。虽然模板多态性很棒,但它并不是唯一的一种。事实上,我有时会发现自己想要一个更少依赖模板的标准库,而更多地依赖于类层次结构来实现多态行为。有时,忽略差异确实是有意义的。事实上,如果不忽略实现细节,还有什么其他原因可以拥有抽象类之类的东西(真的,拥有类层次结构还有什么其他原因?)。

标签: c++ interface iterator covariance


【解决方案1】:

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

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

对于最简单的类型擦除形式,您可以查看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-&gt; 被迭代应用,直到获得一个原始指针,并且您要确保您的类型擦除行为不会破坏该不变量或记录您如何破坏它(即您的适配器可以限制类型 T换行)

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

【讨论】:

  • 我认为这是我首先解决问题的方法。我有beginend,因为当您知道容器是什么类型时,您就可以拥有迭代器的所有功能,以及接口中的单独功能。但这就像你说的那样非常混乱,我正在寻找更好的解决方案。
  • @Seth 你要解决的问题并不简单,这意味着解决方案会很复杂。我没有提到的一些事情,我不确定它们是否显而易见,包括您不需要创建特定的容器层次结构,而是使用any_iterator 来包装常规迭代器。无论如何,简单的解决方案就是使用模板,我建议您这样做,但如果您真的想摆脱这种情况,这就是方法。
  • @David 好吧,因为我不相信有一种方法可以让我满意,所以我将其标记为答案。感谢大家的努力和时间。
  • @Seth:我认为链接的文章和代码值得一读,无论您是否真的想尝试一下,或者只是尝试了解问题的复杂性。类型擦除本身并不是那么复杂的,但是这个特殊的问题是对一组类型族执行类型擦除,同时保持一个通用接口,这使得它更难。
【解决方案2】:

我建议看看Visitor 模式。

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

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 实现是无状态的,例如,实现“过滤”迭代器将变得不可能。

【讨论】:

  • 是的,我想要得到的是,当您知道容器的类型时,您可以获得迭代器的所有功能,但是当您不知道时,您仍然可以访问基本的功能。
  • 这可以通过多个Iterator 类来实现,每个“类型”容器一个(我猜:ForwardIteratorBidirectionalIteratorRandomAccessIterator)。您应该避免它们之间的继承,以保留值语义,但是如果您为相应的Interfaces 定义继承链,那么您可以提供转换构造函数,甚至可以模拟static_castdynamic_cast 功能。
【解决方案3】:

您是否考虑过使用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);
}

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

【讨论】:

  • 这是一个绝妙的主意,我尝试过以这种方式实现它,但有一个症结所在:假设SomeClass 源自Container 和内部名为MyIt 的类派生自IInterfaceContainer 表示它有一个名为 begin 的方法,该方法返回 IInterface。我遇到的问题是,当调用者知道它是SomeClass 时,它应该得到一个MyIt(无论是引用还是值),但是当它只知道它是一个Container 指针时,它应该返回一个@ 987654343@(无论是引用还是值)。这就是为什么我需要协方差。还能做到吗?
  • @Ken 和 iammilind,非常感谢您的帮助,我明天会带着新的想法回来看看这是否可以解决。
  • @Seth,在某种程度上是可能的。正如我在回答中提到的那样,无论IInterface 走到哪里,template 语法都将继续遵循。您必须像这样更改代码的设计。这只是其中一个想法。
【解决方案4】:

正如你所说,问题在于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 数据抽象原理中最重要的一点。

编辑

就实现Containerbeginend 方法而言,这也应该不会太难,但需要对Container 进行一些更改。

首先,让我们向Something 添加一个私有构造函数,它接受一个指向起始对象的指针。我们还将让MyContainer 成为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;
};

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

当然,由于Container现在接受另一个类型参数,我们需要对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);
}

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

【讨论】:

  • 这个答案有很好的观点,这基本上就是 STL 迭代器的工作方式。但是,问题是如何实现Container 接口的beginend?就像现在一样,我必须返回一个IInterface&amp;,但要做到这一点,Somethings 需要存在于某个地方,这就是现在发生的情况。但是为了他们的存在......等等。我能够正确地沟通吗?
  • 我看看能不能加点东西。如果它不是您要找的东西,请告诉我。
  • 这非常接近我正在寻找的 - 我相信虚拟方法与 CRTP 的组合 - 但是,它的设置方式,您需要知道容器的类型才能接受迭代器作为一个论点,这违背了所有这些跳圈的目的。
【解决方案5】:

如果用法应该类似于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.
};

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-01-23
    • 2013-08-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多