18

看过一些相关的问题,但不是这个确切的问题......

我已经将类视为适合几个主要类别,为了简单起见,让我们说这四个:

  • 具有一些数据和一堆操作的值类。它们可以被复制并有意义地比较是否相等(通过 预期副本相等==)。 这些几乎总是缺少虚拟方法。

  • 其实例具有您禁用分配和复制的标识的唯一类。通常没有operator==这些,因为您将它们作为指针而不是对象进行比较。 这些通常有很多虚拟方法,因为不存在的风险,因为您被迫通过指针或引用传递它们。

  • Unique-but-Clonable Classes禁用复制,但如果这是您真正想要的,则预先设计为支持克隆。 这些具有虚拟方法,最重要的是那些遵循虚拟构造/克隆习语的方法

  • 容器类继承了它们所持有的任何东西的属性。 这些往往没有虚拟方法......例如,请参阅“为什么 STL 容器没有虚拟析构函数?” .

不管持有这种非正式的信念系统,有几次我尝试将虚拟方法添加到可复制的东西上。虽然我可能认为它“如果有效的话会很酷”,但它不可避免地会坏掉。

这让我想知道是否有人有一个具有虚拟方法且不禁用复制的类型的实际好例子?

4

4 回答 4

7

我拥有的唯一反例是旨在堆栈分配而不是堆分配的类。我使用它的一种方案是依赖注入:

class LoggerInterface { public: virtual void log() = 0; };

class FileLogger final: public LoggerInterface { ... };

int main() {
    FileLogger logger("log.txt");

    callMethod(logger, ...);
}

这里的关键点是final关键字,这意味着复制 aFileLogger不会导致对象切片。

但是,它可能只是final变成FileLogger了一个 Value 类。

注意:我知道,复制记录器似乎很奇怪......

于 2013-11-04T18:04:30.400 回答
6

能够复制多态类本身并没有错。问题是能够复制非叶类。对象切片会帮你。

一个好的经验法则是永远不要从具体类派生。这样,非叶类自动不可实例化,因此不可复制。不过,为了安全起见,禁用它们中的分配不会有什么坏处。

当然,通过虚函数复制对象并没有错。这种复制是安全的。

多态类通常不是“值类”,但它确实发生了。std::stringstream想到了。它不可复制,但它是可移动的(在 C++11 中),移动与切片的复制没有什么不同。

于 2013-12-01T06:55:39.713 回答
3

虚拟调度发生在运行时。人们想要它的唯一原因是,直到运行时才能知道对象的实际动态类型。如果您在编写程序时已经知道所需的动态类型,则可以使用不同的非虚拟技术(例如模板或非多态继承)来构造您的代码。

需要运行时类型的一个很好的例子是解析 I/O 消息或处理事件——任何一种情况,其中一种或另一种方式,你要么有某种大的开关表来选择正确的具体类型,要么你写你自己的注册和调度系统,它基本上重新发明了多态性,或者你只是使用虚拟调度。

(让我插一句警告:许多人滥用虚函数来解决不需要它们的问题,因为它们不是动态的。当心并批评你所看到的。)

话虽如此,现在很清楚您的代码将主要处理多态类,例如在函数接口或容器中。所以让我们重新表述这个问题:这样的基类应该是可复制的吗?好吧,因为你从来没有实际的、最派生的基对象(即基类本质上是抽象的),这不是一个真正的问题,也没有必要这样做。您已经提到了“克隆”习语,它是在多态中复制的适当类似物。

现在,“克隆”功能必须在每个叶类中实现,并且它必然需要复制叶类。所以是的,可克隆层次结构中的每个叶类都是具有虚函数复制构造函数的类。而且由于派生类的复制构造函数需要复制其基子对象,因此所有基类也必须是可复制的。

所以,现在我相信我们已经将问题提炼为两种可能的情况:要么你的类层次结构中的所有内容都是完全不可复制的,要么你的层次结构支持克隆,因此其中的每个类都必然是可复制的。

那么具有虚函数的类应该有一个复制构造函数吗?绝对地。(这回答了你最初的问题:当你将你的类集成到一个可克隆的多态层次结构中时,你会向它添加虚函数。)

您是否应该尝试从基本参考中复制?可能不是。

于 2013-12-02T09:33:22.157 回答
2

不是一个,而是两个类:

#include <iostream>
#include <vector>
#include <stdexcept>

class Polymorph
{
    protected:
    class Implementation {
        public:
        virtual ~Implementation() {};
        // Postcondition: The result is allocated with new.
        // This base class throws std::logic error.
        virtual Implementation* duplicate() {
             throw std::logic_error("Duplication not supported.");
        }

        public:
        virtual const char* name() = 0;
    };

    // Precondition: self is allocated with new.
    Polymorph(Implementation* self)
    :   m_self(self)
    {}

    public:
    Polymorph(const Polymorph& other)
    :   m_self(other.m_self->duplicate())
    {}

    ~Polymorph() {
        delete m_self;
    }

    Polymorph& operator = (Polymorph other) {
        swap(other);
        return *this;
    }

    void swap(Polymorph& other) {
        std::swap(m_self, other.m_self);
    }

    const char* name() { return m_self->name(); }

    private:
    Implementation* m_self;
};

class A : public Polymorph
{
    protected:
    class Implementation : public Polymorph::Implementation
    {
        protected:
        Implementation* duplicate() {
            return new Implementation(*this);
        }

        public:
        const char* name() { return "A"; }
    };

    public:
    A()
    :   Polymorph(new Implementation())
    {}
};

class B : public Polymorph {
    protected:
    class Implementation : public Polymorph::Implementation {
        protected:
        Implementation* duplicate() {
            return new Implementation(*this);
        }

        public:
        const char* name() { return "B"; }
    };

    public:
    B()
    :   Polymorph(new Implementation())
    {}
};


int main() {
    std::vector<Polymorph> data;
    data.push_back(A());
    data.push_back(B());
    for(auto x: data)
        std::cout << x.name() << std::endl;
    return 0;
}

注意:在这个例子中,对象总是被复制的(尽管你可以实现共享语义)

于 2013-11-04T18:34:24.303 回答