Могу ли я иметь полиморфные контейнеры с семантикой значений в C ++? - PullRequest
31 голосов
/ 03 сентября 2008

Как правило, я предпочитаю использовать значение, а не семантику указателя в 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

}

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

Ответы [ 9 ]

24 голосов
/ 03 сентября 2008

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

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

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

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

10 голосов
/ 16 сентября 2008

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

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

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

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

10 голосов
/ 05 сентября 2008

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

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

3 голосов
/ 03 сентября 2008

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

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

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

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

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

3 голосов
/ 03 сентября 2008

Вы также можете рассмотреть 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! Просто подумал, что это может быть интересно.

2 голосов
/ 03 сентября 2008

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

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

2 голосов
/ 03 сентября 2008

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

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

1 голос
/ 16 сентября 2008

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

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

0 голосов
/ 08 апреля 2018

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

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

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

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

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. Однако он больше не будет копироваться, и вам придется либо перемещать все объекты, либо осуществлять копирование вручную. Шон Родитель подробно обсуждает это в своем выступлении, а реализация показана в вышеупомянутом ответе.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...