Подтвердить что ты не робот

Действительно ли хорошая идея ставить виртуальные методы на тип с возможностью копирования?

Увидели некоторые связанные вопросы, но не этот точный...

Я рассматривал классы как подходящие для нескольких основных категорий, скажем, эти четыре для простоты:

  • Классы значений, которые имеют некоторые данные и множество операций. Они могут быть скопированы и осмысленно сопоставлены для равенства (при этом копии должны быть равны через ==). Этим почти всегда не хватает виртуальных методов.

  • Уникальные классы, чьи экземпляры имеют идентификатор, который вы отключите назначение и копирование. Обычно на них нет operator==, потому что вы сравниваете их как указатели, а не как объекты. У них довольно часто много виртуальных методов, так как нет риска , поскольку вы вынуждены передавать их указателем или ссылкой.

  • Уникальные, но клонируемые классы, которые запрещают копирование, но предварительно разработаны для поддержки клонирования, если это то, что вы действительно хотите. У них есть виртуальные методы, наиболее важные из которых следуют виртуальная конструкция/клонирование идиомы

  • Контейнерные классы, которые наследуют свойства того, что они держат. Они, как правило, не имеют виртуальных методов... см. например "Почему контейнеры STL не имеют виртуальных деструкторов?" .

Независимо от того, чтобы придерживаться этой неофициальной системы убеждений, пару раз я пытался добавить виртуальный метод к чему-то, что можно скопировать. Хотя я, возможно, подумал, что "будет действительно здорово, если это сработает", неизбежно это ломается.

Это заставило меня задаться вопросом, есть ли у кого-нибудь хороший пример того типа, который имеет виртуальные методы и не отключает копирование?

4b9b3361

Ответ 1

Нет ничего естественного в том, чтобы копировать полиморфный класс. Проблема заключается в возможности копирования не-листового класса. Обрезка объектов поможет вам.

Хорошее эмпирическое правило никогда не вытекает из конкретного класса. Таким образом, неклассические классы автоматически не подлежат копированию и, следовательно, не копируются. Это не помешает отключить назначение в них, хотя, чтобы быть в безопасности.

Конечно, нет ничего плохого в копировании объекта с помощью виртуальной функции. Этот вид копирования безопасен.

Полиморфные классы обычно не являются "классами значений", но это происходит. std::stringstream приходит на ум. Он не может быть скопирован, но он движимый (в С++ 11), и перемещение ничем не отличается от копирования в отношении разрезания.

Ответ 2

Единственный встречный пример, который у меня есть, - это классы, предназначенные для выделения стека, а не для выделения кучи. Одна из схем, для которой я использую это, - Инъекция зависимостей:

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

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

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

    callMethod(logger, ...);
}

Ключевым моментом здесь является ключевое слово final, это означает, что копирование FileLogger не может привести к обрезке объектов.

Однако может быть просто, что final превратил FileLogger в класс Value.

Примечание. Я знаю, что копирование регистратора кажется странным...

Ответ 3

Виртуальная отправка происходит во время выполнения. Единственная причина, по которой это нужно, - это когда фактический динамический тип объекта не может быть известен до времени выполнения. Если вы уже знаете желаемый динамический тип при написании программы, вы можете использовать разные, не виртуальные методы (например, шаблоны или неполиморфное наследование) для структурирования вашего кода.

Хорошим примером для необходимости во время набора текста является разбор сообщений ввода-вывода или обработка событий – любая ситуация, когда так или иначе у вас будет какой-то большой стол для переключения, чтобы выбрать правильный конкретный тип, или вы пишете свою систему регистрации и отправки, которая в основном восстанавливает полиморфизм, или вы просто используете виртуальную отправку.

(Позвольте мне включить предупреждение: многие люди неправильно используют виртуальные функции для решения проблем, которые им не нужны, потому что они не являются динамическими. Остерегайтесь и критикуйте то, что вы видите.)

С учетом сказанного теперь ясно, что ваш код будет работать в основном с полиморфными базовыми классами, например. в функциональных интерфейсах или в контейнерах. Поэтому давайте перефразируем вопрос: должен ли такой базовый класс быть скопирован? Ну, так как у вас никогда не было реальных базовых объектов (т.е. Базовый класс по сути абстрактен), это не проблема, и нет необходимости в этом. Вы уже упоминали о "клонированной" идиоме, которая является соответствующим аналогом копирования в полиморфной форме.

Теперь функция "клонировать" обязательно реализуется в каждом листовом классе и обязательно требует копирования листовых классов. Поэтому да, каждый листовой класс в клонируемой иерархии - это класс с виртуальными функциями и конструктор копий. А поскольку конструктор копирования производного класса должен скопировать свои базовые подобъекты, все базы тоже должны быть скопируемыми.

Итак, теперь я считаю, что мы перевели проблему до двух возможных случаев: либо все в вашей иерархии классов совершенно невозможно, либо ваша иерархия поддерживает клонирование, и, следовательно, по необходимости каждый класс в ней может быть скопирован.

Итак, должен ли класс с виртуальными функциями иметь конструктор копирования? Абсолютно. (Это отвечает на ваш первоначальный вопрос: когда вы интегрируете свой класс в клонированную, полиморфную иерархию, вы добавляете к нему виртуальные функции.)

Должны ли вы попытаться сделать копию из базовой ссылки? Наверное, нет.

Ответ 4

Не с одним, а с двумя классами:

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

Примечание. В этом примере объекты копируются всегда (вы можете реализовать общую семантику, хотя)