【问题标题】:Covariant return type with non-pointer/reference return type具有非指针/引用返回类型的协变返回类型
【发布时间】:2012-05-25 17:52:14
【问题描述】:

我正在尝试在 C++(11) 中实现类似集合类的 .NET 框架。我的问题是无效的协变类型。我有这些课程:

template<typename T>
class IEnumerator
{
public:
    virtual bool MoveNext() = 0;
    //...
};

template<typename T>
class IEnumerable
{
    virtual IEnumerator<T> GetEnumerator() = 0;
};

template<typename T>
class List : public IEnumerable<T>
{
public:
    struct Enumerator : public IEnumerator<T>
    {
        Enumerator(List<T> &list)
        {
            //...
        }
        // ...
    };

    Enumerator GetEnumerator()
    {
        return Enumerator(*this);
    }
};

在我看来,这太棒了。但是在 C++ 中实现它看起来是不可能的。我得到 g++ 的“无效协变返回类型”,据我所知,问题是 GetEnumerator 可能只返回一个指针或对 Enumerator 的引用,而不是 Enumerator 本身的对象。

我想避免返回这样的指针:

Enumerator *GetEnumerator()
{
    return new Enumerator(*this);
}

因为我不希望调用者打扰删除。使用临时对象,我可以确保该对象被自动删除,因为它不再需要了。使用引用可能会更糟。

我错过了什么吗?还是 C++ 标准(和语言)存在巨大漏洞?我真的很想实现这样的目标。

提前致谢。

【问题讨论】:

  • 啊! IEnumerable 是对 C++ 迭代器的糟糕改造;为什么要在 C++ 中使用它?在任何情况下,.NET 都使用引用语义,因此要在 C++ 中模仿这一点,您必须使用引用或指针——这并不奇怪。
  • 这是不可能用价值语言实现的。
  • @ildjarn 我觉得很好,这样我就可以在函数参数中使用 IEnumerable,无论后面的类型是什么(向量、链表、RB-tree)它仍然是 IEnumerable,它有返回 IEnumerator 的 GetEnumerator() 函数。您可以将 vector 和 list 都传递给没有共同祖先的同一个函数吗?
  • @AlfaOmega08: 是的,你可以编写一个函数(模板),它可以使用list&lt;T&gt;vector&lt;T&gt;,而不会出现任何问题——但你几乎不应该这样做。相反,它通常应该采用一对迭代器(或一个范围)。标准库有很多示例(例如,std::accumulatestd::sortstd::findstd::lower_bound 等)
  • 顺便说一句,作为返回调用者必须删除的新 T* 的安全替代方法,您可以返回 std::shared_ptr,它会在所有引用被删除时自动删除已失效。

标签: c++ pointers ienumerable return-type covariant


【解决方案1】:

无法实现协变值返回类型。问题是调用者有责任在堆栈中为返回的对象分配空间,而协变值返回所需的空间量在编译时是未知的。

这与指针/引用无缝协作,因为返回的对象是指针或引用(而不是实际的派生对象),并且大小在编译时是已知的。

在与@curiousguy 进行了相当荒谬的(就我而言)讨论之后,我必须从之前的答案中回溯。没有技术问题会使协变值返回类型成为不可能。另一方面,它会产生不同的负面影响:

从设计的角度来看,如果从基类调用返回的对象,则必须切片(这是返回对象的大小很重要的地方)。这与当前模型有明显的区别,在当前模型中函数always返回same对象,改变类型的只是引用或指针。但实际的object是一样的。

在一般情况下,协变值类型会抑制某些复制省略优化。目前,对于按值返回的函数,许多调用约定规定调用者将指针传递给返回对象的位置。这允许调用者保留将保存该值的变量空间,然后传递该指针。然后,被调用者可以使用该指针来代替将在调用者上下文中保存值的对象进行构造,并且不需要副本。使用协变值返回类型,并且由于必须销毁最终覆盖器创建的最派生对象以避免未定义的行为。调用者会传递一个指向内存中某个位置的指针,trampoline 函数必须为最终覆盖器的返回对象保留空间,然后它需要从第二个对象 slice-copy 到首先,产生复制成本。

无论如何,操作的实际成本不会像调用最终覆盖程序的语义不同的事实那样成为问题* sup> 取决于执行调用的引用的静态类型。


* 这已经是当前语言定义的情况。对于所有非虚函数,如果派生类型隐藏基上的成员函数,则返回指针/引用的静态类型(这又取决于用于调用虚函数的静态类型) function) 将影响实际调用的函数并且行为不同。

【讨论】:

  • "我已经告诉过你,关于虚函数你是对的,这意味着"好的,暗示道歉。有趣的概念。 “话虽如此,你的态度和阅读有问题” 不,你有问题。 “你还没有理解 2 阶段查找的精髓。”我知道你在拖钓。
  • "你刚才在评论中说,有时决定是不支持的,而且是任意的(与你之前所说的相反,但放手吧)。" 决定不是凭空而来的。 委员会成员在投票时会考虑不同的事情(有时,他们会出于不同的原因做出相同的决定)。大多数委员会的决定都有记录的动机(在 WG21 文件中)。 (在 D&E 中用于 pre-std 流程决策。)当我说“x 是任意限制”时,我的意思是您不能根据其他更基本的设计决策来证明限制的合理性。
  • 有时人们只是担心功能的泛化会使事情变得过于复杂。有时人们只是没有时间评估概括的后果。委员会有许多棘手的问题需要考虑。根据经验,许多人认为尽可能地概括事物是危险的,并且往往会导致复杂的半生不熟的设计。请参阅 STL 分配器,以了解 1997 年大多数委员会认为是一场完整的设计灾难的示例,不仅试图概括 malloc,还试图概括指针、引用等。
  • @JohannesSchaub-litb 是的,但上次是个陷阱,还有一群愤怒的暴徒。
【解决方案2】:
template<typename T>
class IEnumerable
{
    virtual IEnumerator<T> GetEnumerator() = 0;
};

你试图返回一个IEnumerable&lt;T&gt;,但它是一个抽象基类:这意味着你承诺构造一个无法实例化的类的对象!

只能实例化派生自抽象基类的具体类

您可能打算返回指向此类对象的指针。无论如何,这是一个糟糕的设计。您不需要在 C++ 中模拟 Java。

【讨论】:

    【解决方案3】:

    除非你使用指针,否则你不会在 C++ 中做类似的事情。 .net 家伙使用引用,所以它几乎是相同的东西。

    在 C++ 中,您更有可能使用概念而不是继承来实现这一点。您应该审查“通用编程”的想法。 boost网站有一个不错的介绍:http://www.boost.org/community/generic_programming.html

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-02-06
      • 2011-12-10
      • 1970-01-01
      • 2015-11-03
      • 2018-12-08
      • 1970-01-01
      • 2011-09-02
      • 1970-01-01
      相关资源
      最近更新 更多