Могу ли я иметь полиморфные контейнеры с семантикой значений в C ++?

StackOverflow https://stackoverflow.com/questions/41045

  •  09-06-2019
  •  | 
  •  

Вопрос

Как правило, я предпочитаю использовать значение, а не семантику указателя в C ++ (т.е. использовать vector<Class> вместо vector<Class*>). Обычно небольшая потеря производительности более чем компенсируется отсутствием необходимости помнить об удалении динамически размещаемых объектов.

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

#include <iostream>

using namespace std;

class Parent
{
    public:
        Parent() : parent_mem(1) {}
        virtual void write() { cout << "Parent: " << parent_mem << endl; }
        int parent_mem;
};

class Child : public Parent
{
    public:
        Child() : child_mem(2) { parent_mem = 2; }
        void write() { cout << "Child: " << parent_mem << ", " << child_mem << endl; }

        int child_mem;
};

int main(int, char**)
{
    // I can have a polymorphic container with pointer semantics
    vector<Parent*> pointerVec;

    pointerVec.push_back(new Parent());
    pointerVec.push_back(new Child());

    pointerVec[0]->write(); 
    pointerVec[1]->write(); 

    // Output:
    //
    // Parent: 1
    // Child: 2, 2

    // But I can't do it with value semantics

    vector<Parent> valueVec;

    valueVec.push_back(Parent());
    valueVec.push_back(Child());    // gets turned into a Parent object :(

    valueVec[0].write();    
    valueVec[1].write();    

    // Output:
    // 
    // Parent: 1
    // Parent: 2

}

Мой вопрос: могу ли я иметь свой торт (семантика значения) и есть его тоже (полиморфные контейнеры)? Или я должен использовать указатели?

Это было полезно?

Решение

Поскольку объекты разных классов будут иметь разные размеры, вы столкнетесь с проблемой нарезки, если сохраните их как значения.

Одним из разумных решений является хранение интеллектуальных указателей в контейнерах. Я обычно использую boost :: shared_ptr, который безопасно хранить в контейнере. Обратите внимание, что std :: auto_ptr - это не так.

vector<shared_ptr<Parent>> vec;
vec.push_back(shared_ptr<Parent>(new Child()));

shared_ptr использует подсчет ссылок, поэтому он не удалит базовый экземпляр, пока не будут удалены все ссылки.

Другие советы

Да, вы можете.

Библиотека boost.ptr_container предоставляет семантические версии полиморфных значений стандартных контейнеров. Вам нужно только передать указатель на объект, выделенный из кучи, и контейнер переходит во владение, и все дальнейшие операции будут обеспечивать семантику значения, за исключением восстановления владения, которое дает вам почти все преимущества семантики значения с помощью интеллектуального указателя. .

Я просто хотел указать на этот вектор < Foo > обычно более эффективен, чем vector < Foo * > ;. В векторе & Lt; Foo & Gt; все Foos будут находиться в памяти рядом друг с другом. Предполагая холодный TLB и кеш, первое чтение добавит страницу в TLB и вытянет часть вектора в L # кеши; последующие чтения будут использовать теплый кеш и загруженный TLB, со случайными пропусками кеша и менее частыми сбоями TLB.

Сравните это с вектором < Foo * > ;: Когда вы заполните вектор, вы получите Foo * из вашего распределителя памяти. Предполагая, что ваш распределитель не очень умный, (tcmalloc?) Или вы заполняете вектор медленно с течением времени, расположение каждого Foo, вероятно, будет далеко от других Foo: возможно, всего на сотни байтов, может быть, мегабайт друг от друга.

В худшем случае, когда вы сканируете через вектор < Foo * > и разыменовывая каждый указатель, вы столкнетесь с ошибкой TLB и отсутствием кэша - это в конечном итоге будет много медленнее, чем если бы у вас был вектор < Foo > ;. (Что ж, в самом худшем случае каждый Foo был выгружен на диск, и при каждом чтении с диска выполняются запросы seek () и read () для перемещения страницы обратно в ОЗУ.)

Итак, продолжайте использовать vector < Foo > когда уместно. : -)

Большинство типов контейнеров хотят абстрагировать конкретную стратегию хранения, будь то связанный список, вектор, дерево или что у вас есть. По этой причине у вас будут проблемы как с обладанием, так и с употреблением вышеупомянутого торта (т. Е. Торта это ложь (NB: кто-то должен был пошутить)).

Так что же делать? Ну, есть несколько симпатичных вариантов, но большинство из них сводятся к вариантам на одну из нескольких тем или их комбинациям: выбор или придумывание подходящего умного указателя, умный способ игры с шаблонами или шаблонами шаблонов, использование общего интерфейса для контактеров. это обеспечивает ловушку для реализации двойной отправки на каждого загрязнителя.

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

В прошлом я потратил некоторое время, пытаясь представить, как использовать Virtual Proxy / Envelope-Letter / этот симпатичный трюк с указателями с подсчетом ссылок для достижения чего-то вроде основы для семантического программирования значений в C ++.

И я думаю, что это можно было бы сделать, но вам нужно было бы обеспечить достаточно замкнутый мир, подобный C # -управляемому коду, внутри C ++ (хотя бы тот, из которого вы могли бы прорваться к базовому C ++, когда это необходимо). Поэтому я очень сочувствую вашей мысли.

Вы также можете рассмотреть возможность boost :: any . Я использовал это для гетерогенных контейнеров. Когда вы читаете значение обратно, вам нужно выполнить any_cast. В случае сбоя он выдаст bad_any_cast. Если это произойдет, вы можете поймать и перейти к следующему типу.

Я верю , он выдаст bad_any_cast, если вы попытаетесь any_cast производного класса к его базе. Я попробовал это:

  // But you sort of can do it with boost::any.

  vector<any> valueVec;

  valueVec.push_back(any(Parent()));
  valueVec.push_back(any(Child()));        // remains a Child, wrapped in an Any.

  Parent p = any_cast<Parent>(valueVec[0]);
  Child c = any_cast<Child>(valueVec[1]);
  p.write();
  c.write();

  // Output:
  //
  // Parent: 1
  // Child: 2, 2

  // Now try casting the child as a parent.
  try {
      Parent p2 = any_cast<Parent>(valueVec[1]);
      p2.write();
  }
  catch (const boost::bad_any_cast &e)
  {
      cout << e.what() << endl;
  }

  // Output:
  // boost::bad_any_cast: failed conversion using boost::any_cast

Несмотря на это, я бы сначала пошел по пути shared_ptr! Просто подумал, что это может быть интересно.

  

Посмотрите static_cast и reinterpret_cast
  В языке программирования C ++, 3-е издание, Бьярн Страуструп описывает его на стр. 130. Об этом есть целый раздел в главе 6.
  Вы можете преобразовать свой родительский класс в дочерний класс. Это требует, чтобы вы знали, когда каждый из которых. В книге доктор Страуструп рассказывает о различных методах, позволяющих избежать этой ситуации.

Не делай этого. Это сводит на нет полиморфизм, который вы пытаетесь достичь в первую очередь!

Просто чтобы добавить одну вещь ко всем 1800 ИНФОРМАЦИЯ уже сказано.

Возможно, вы захотите взглянуть на & цитату «Более эффективный C ++» < !> quot; Скотта Майерса " Пункт 3: Никогда не обрабатывать массивы полиморфно " чтобы лучше понять эту проблему.

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

Это идея, которая может удовлетворить ваши потребности.

При поиске ответа на эту проблему я столкнулся и с этим, и с аналогичный вопрос . В ответах на другой вопрос вы найдете два предложенных решения:

<Ол>
  • Используйте std :: option или boost :: option и шаблон посетителя. Это решение затрудняет добавление новых типов, но легко добавляет новые функциональные возможности.
  • Используйте класс-оболочку, аналогичный тому, что Шон Родитель представляет в своем выступлении . Это решение затрудняет добавление новых функций, но позволяет легко добавлять новые типы.
  • Оболочка определяет интерфейс, который вам нужен для ваших классов, и содержит указатель на один такой объект. Реализация интерфейса осуществляется с помощью бесплатных функций.

    Вот пример реализации этого шаблона:

    class Shape
    {
    public:
        template<typename T>
        Shape(T t)
            : container(std::make_shared<Model<T>>(std::move(t)))
        {}
    
        friend void draw(const Shape &shape)
        {
            shape.container->drawImpl();
        }
        // add more functions similar to draw() here if you wish
        // remember also to add a wrapper in the Concept and Model below
    
    private:
        struct Concept
        {
            virtual ~Concept() = default;
            virtual void drawImpl() const = 0;
        };
    
        template<typename T>
        struct Model : public Concept
        {
            Model(T x) : m_data(move(x)) { }
            void drawImpl() const override
            {
                draw(m_data);
            }
            T m_data;
        };
    
        std::shared_ptr<const Concept> container;
    };
    

    Различные формы затем реализуются как обычные структуры / классы. Вы можете сами выбирать, хотите ли вы использовать функции-члены или бесплатные функции (но вам придется обновить вышеуказанную реализацию, чтобы использовать функции-члены). Я предпочитаю бесплатные функции:

    struct Circle
    {
        const double radius = 4.0;
    };
    
    struct Rectangle
    {
        const double width = 2.0;
        const double height = 3.0;
    };
    
    void draw(const Circle &circle)
    {
        cout << "Drew circle with radius " << circle.radius << endl;
    }
    
    void draw(const Rectangle &rectangle)
    {
        cout << "Drew rectangle with width " << rectangle.width << endl;
    }
    

    Теперь вы можете добавлять оба объекта Circle и Rectangle в один и тот же std::vector<Shape>:

    int main() {
        std::vector<Shape> shapes;
        shapes.emplace_back(Circle());
        shapes.emplace_back(Rectangle());
        for (const auto &shape : shapes) {
            draw(shape);
        }
        return 0;
    }
    

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

    int main() {
        Shape a = Circle();
        Shape b = Rectangle();
        b = a;
        draw(a);
        draw(b);
        return 0;
    }
    

    Это производит:

    Drew rectangle with width 2
    Drew rectangle with width 2
    

    Если вас беспокоит shared_ptr, вы можете заменить его на unique_ptr. Однако он больше не будет копироваться, и вам придется либо перемещать все объекты, либо осуществлять копирование вручную. Шон Родитель подробно обсуждает это в своем выступлении, а реализация показана в вышеупомянутом ответе.

    Лицензировано под: CC-BY-SA с атрибуция
    Не связан с StackOverflow
    scroll top