Может ли быть хорошей идеей помещать виртуальные методы в копируемый тип?

Видели несколько связанных вопросов, но не этот ...

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

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

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

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

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

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

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


person HostileFork says dont trust SE    schedule 04.11.2013    source источник
comment
Вы могли бы сказать, что класс-контейнер морально является классом-ценностью, просто если его содержимое не копируется, оно не может быть скопировано. Я понимаю, что есть тонкая разница между высказыванием этого и высказыванием того, что вы сказали, я просто имею в виду, что контейнеры в целом не наследуют категорию в этой таксономии, это одна конкретная вещь.   -  person Steve Jessop    schedule 04.11.2013
comment
Мне не совсем ясно, когда вы говорите виртуальные методы для чего-то копируемого, вы имеете в виду вашу уникальную, но клонируемую категорию или что-то еще? Как ваша ценностная категория, но с виртуальными функциями.   -  person stonemetal    schedule 05.11.2013
comment
@stonemetal Независимо от моей категории, мой вопрос в конце. Я буквально спрашиваю, будет ли лучше какой-либо класс с виртуальными методами, если вы сделаете его не копируемым (или, альтернативно, измените его, чтобы не использовать какие-либо виртуальные методы).   -  person HostileFork says dont trust SE    schedule 05.11.2013
comment
Поучительно было бы рассказать, как именно ломается.   -  person n. 1.8e9-where's-my-share m.    schedule 01.12.2013
comment
@ n.m. Что ж, термин нарезка объектов как бы затрагивает его суть ... дело в том, что вы идете делать копию, и эта копия не действует как оригинал ...   -  person HostileFork says dont trust SE    schedule 01.12.2013


Ответы (4)


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

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

Конечно, нет ничего плохого в копировании объекта через виртуальную функцию. Такое копирование безопасно.

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

person n. 1.8e9-where's-my-share m.    schedule 01.12.2013
comment
А вот несколько поддерживающих цитат к практическому правилу: Херб Саттер: «Не производите от конкретных классов». (« Виртуальность »). Скотт Мейерс: «Сделайте классы, не являющиеся листовыми, абстрактными». (Более эффективный C ++, пункт 33). - person gx_; 01.12.2013
comment
Совпадает ли это случайно с утверждением, что после того, как вы предоставили реализации всех абстрактных виртуальных функций в классе до такой степени, что они могут быть созданы, они должны стать окончательными, если они поддерживают копирование (??) - person HostileFork says dont trust SE; 06.12.2013
comment
@HostileFork да, принцип тот же. - person n. 1.8e9-where's-my-share m.; 07.12.2013

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

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.

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

person Matthieu M.    schedule 04.11.2013
comment
Думаю, вы правы, FileLogger как написано - это класс значений. И это хороший пример того, почему это не противоречит реализации полиморфного интерфейса. Пока интерфейс также запрещает копирование пользователями (что он и делает, поскольку это абстрактный класс), вы защищаете их от очевидной ловушки. Я почти уверен, что вы могли бы придумать пример, который менее странно копировать, чем регистратор, нам нужно, чтобы копирование было естественной частью интерфейса конкретного класса, но не полиморфным интерфейсом. Это означает, что идеальный пример - это, очевидно, хорошая идея. - person Steve Jessop; 04.11.2013
comment
Ах, вы видите это чаще в Java, чем в C ++, но это не является чем-то необычным. Классы могут реализовать некоторый интерфейс полиморфного слушателя, чтобы они могли передавать себя в механизм уведомления, который им небезразличен. Тип значения может захотеть сделать это независимо от того, что он может быть скопирован сам, хотя вы можете беспокоиться, что это немного неочевидно, распространяется ли факт регистрации на копии! - person Steve Jessop; 04.11.2013
comment
Спасибо за термин нарезки объектов, раньше не слышал этого названия ... (добавлен тег). Я действительно задавался вопросом, может ли быть полезным использование виртуальных методов для внутреннего протокола, а затем установить его как final до того, как он просочится ... хотя я не мог придумать хорошего примера. Написание класса значений, который должен соответствовать интерфейсу, похоже, может быть одним (хотя этот конкретный случай кажется немного запутанным) ... - person HostileFork says dont trust SE; 04.11.2013
comment
@ DieterLücking: Я никогда не говорил, что существует только один или что существует только один уровень наследования (хотя я признаю, что это то, что я представил). Акцент делается на final, который фактически предотвращает дальнейшее расширение и, таким образом, каким-то образом устраняет виртуальность методов. - person Matthieu M.; 05.11.2013
comment
Зачем вам там последнее ключевое слово? Без него будет работать. - person BЈовић; 02.12.2013
comment
@ BЈовић: Да, будет; однако final подчеркивает, что FileLogger является классом значений (больше не может быть производным от). - person Matthieu M.; 02.12.2013

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

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

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

Теперь ясно, что ваш код будет иметь дело в основном с полиморфными базовыми классами, например в интерфейсах функций или в контейнерах. Итак, давайте перефразируем вопрос: должен ли такой базовый класс быть копируемым? Что ж, поскольку у вас никогда не бывает реальных, наиболее производных базовых объектов (т.е. базовый класс по сути является абстрактным), это не проблема, и в этом нет необходимости. Вы уже упомянули идиому «клонировать», которая является подходящим аналогом копирования в полиморфном.

Теперь функция «клонирования» обязательно реализована в каждом листовом классе и обязательно требует копирования конечных классов. Итак, да, каждый конечный класс в клонируемой иерархии является классом с виртуальными функциями и конструктором копирования. А поскольку конструктор копирования производного класса должен копировать свои базовые подобъекты, все базы также должны быть копируемыми.

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

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

Стоит ли пытаться сделать копию с базовой ссылки? Возможно нет.

person Kerrek SB    schedule 02.12.2013
comment
Для меня «копируемый тип» подразумевает общедоступный copy-ctor (и copy-op =), но в клонируемой иерархии их можно сделать непубличными (не private (в по крайней мере, для не-листов), поскольку действительно копирующий-ctor производного класса нуждается в доступе к его базовому copy-ctor, но protected), что, я думаю, именно то, что OP имел в виду под «отключением копирования» (при поддержке клонирования). - person gx_; 04.12.2013

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

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

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

person Community    schedule 04.11.2013
comment
Я не совсем следую примеру, поскольку класс с виртуальными методами (Implementation), похоже, следует шаблону клонирования, передается по указателю и использует подкачку. Не могли бы вы показать использование и как это не сработает, если реализация отключит копирование? - person HostileFork says dont trust SE; 04.11.2013
comment
Этот vector пример напоминает мне презентацию Шона Родителя «Семантика значений и основанный на концепциях полиморфизм» =) Теперь, строго говоря, Polymorph не имеет виртуальных функций-членов, и Implementation может (должен?) Отключить копирование, так что на самом деле это не один класс что «имеет виртуальные методы и не отключает копирование». Но это действительно «полиморфные типы значений» (т. Е. копируемые конкретные типы, которые демонстрируют полиморфное поведение). - person gx_; 01.12.2013