2013-04-07 2 views
2

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

std::vector<Base*> collection; 
collection.push_back(new Derived()); 

Таким образом, выделенные объекты не будут обязательно сохранены бок о бок в памяти, потому что в каждом распределении new() вернет произвольное положение в памяти. Кроме того, есть дополнительный указатель (vptr), встроенный в каждый объект, потому что класс Base, конечно, должен быть полиморфным.

В данном конкретном случае (тип определяется один раз + тип никогда не изменяется), указанное решение не является оптимальным решением, потому что, теоретически,

  1. это не нужно хранить так же vptr (SizeOf() = размер указателя) для каждого объекта: все они указывают на то же самое vtable;
  2. можно использовать непрерывные места хранения, так как размер объектов определяется в начале программы и никогда не будет меняться.

Q: ли вы, ребята, знаете стратегию/контейнер/распределитель памяти/идиому/трюк/что-нибудь еще, чтобы преодолеть эти проблемы?

Я думаю, что я мог бы сделать что-то вроде этого (с использованием классического Shape пример):

struct Triangle_Data { 
    double p1[3],p2[3],p3[3]; 
}; 

struct Circle_Data { 
    double radius; 
}; 

struct Shape { 
    virtual double area() const = 0; 
    virtual char* getData() = 0; 
    virtual ~Shape() {} 
}; 

struct Triangle : public Shape { 
    union { 
     Triangle_Data tri_data; 
     char data[sizeof(Triangle_Data)]; 
    }; 
    double area() const { /*...*/ }; 
    char* getData() { return data; } 
    Triangle(char * dat_) { 
     std::copy(dat_, dat_+sizeof(Triangle_Data), this->data); 
    }; 
}; 

struct Circle : public Shape { 
    union { 
     Circle_Data circ_data; 
     char data[sizeof(Circle_Data)]; 
    }; 
    double area() const { /*...*/ }; 
    char* getData() { return data; } 
    Circle(char * dat_) { 
     std::copy(dat_, dat_+sizeof(Circle_Data), this->data); 
    }; 
}; 

template<class BaseT> 
struct Container { 
    int n_objects; 
    int sizeof_obj; 
    std::vector<char> data; 
    Container(...arguments here...) : ...init here... { 
     data.resize(sizeof_obj * n_objects); 
    } 
    void push_back(Shape* obj) { 
     // copy the content of obj 
     for(int i=0; i<sizeof_obj; ++i) 
      data.push_back(*(obj.getData() + i)); 
    } 
    char* operator[] (int idx) { 
     return data + idx*sizeof_obj; 
    } 
}; 

// usage: 
int main() { 
    Container<Shape> collection(..init here..); 
    collection.push_back(new Circle()); 
    cout << Circle(collection[0]).area() << endl; // horrible, but does it work? 
}; 

Конечно, этот подход имеет много проблем с безопасностью типа, выравнивание и т.п .. Любое предложение ?

Спасибо

+0

Если вы запрашиваете полиморфизм во время выполнения, то есть виртуальные функции, вы не можете не платить цену за _'vptr'_ (уникальная функция _'vtable'_) – StoryTeller

+2

Можно было бы использовать язык для обеспечить эту оптимизацию. C++ - нет. –

+1

«Базовый класс, конечно, должен быть виртуальным». Нет такого понятия, как «виртуальный» класс. Существуют только полиморфные классы (и виртуальные базы, что совсем другое)./nitpick – curiousguy

ответ

1

1) нет необходимости хранить один и тот же vptr (8 байт на объект?) Collection.size() раз;

Это не обязательно, но объекты не зависят друг от друга.

2) можно использовать смежные места хранения, так как их размер известен и равен друг другу, когда их тип определен.

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


Итак, что вы можете сделать?

Одним из решений является не использование полиморфных экземпляров, но вместо того, чтобы иметь данные и полиморфизм отделенный:

struct IShape { 
    virtual ~IShape() {} 
    virtual double area() const = 0; 
}; 

struct Circle { 
    float center, radius; 
}; 

struct IShapeCircle: IShape { 
    IShapeCircle(Circle const& c): circle(const_cast<Circle&>(c)) {} 
    virtual double area() const { return PI * circle.radius * circle.radius; } 

    Circle& circle; 
}; 

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

struct IShapeVector { 
    virtual ~IShapeVector() {} 
    std::unique_ptr<IShape> get(size_t i) = 0; 
    std::unique_ptr<IShape const> get(size_t i) const = 0; 
}; 

struct IShapeCircleVector: IShapeVector { 
    std::unique_ptr<IShape> get(size_t i) { 
     return make_unique<IShapeCircle>(_circles.at(i)); 
    } 
    std::unique_ptr<IShape const> get(size_t i) const { 
     return make_unique<IShapeCircle const>(_circles.at(i)); 
    } 

    std::vector<Circle> _circles; 
}; 

Однако, вы можете обнаружить, что распределение/открепление трафик замедляет вас больше, чем просто у-PTR.

+0

Спасибо !!!! Я попытаюсь реализовать это и опубликовать результат здесь. В моем случае все распределения выполняются один раз. Узким местом являются петли. Моя проблема с 'vptr's заключается в том, что размер объектов не намного больше, чем размер указателя, таким образом, сохранение 8 байтов на объект - большое дело. – montefuscolo

+0

@montefuscolo: если это плотный цикл, то вы можете найти распределение/освобождение (скрытое за 'unique_ptr') довольно неприятным. В этом случае одно решение состоит в том, чтобы сделать «IShapeCircle» «сбрасываемым» (используя указатель вместо ссылки), выделить один за пределами цикла (пустой), а затем «IShapeVector» взять «IShape &», которому нужно назначить 'get' вместо того, чтобы возвращать новый объект:' void IShapeVector :: assign (IShape & is, size_t i) = 0'. Затем, void IShapeCircleVector :: assign (IShape & is, size_t i) {static_cast (is) .reset (_circles.at (i)); } '. –

3

это не нужно хранить такие же виртуальные таблицы (8 байт на объект?) Collection.size() раз;

Вы не храните vtables вообще. Вы сохраняете указатели, которые имеют одинаковый размер, являются ли объекты полиморфными или нет. Если коллекция имеет N объектов, то она принимает N * sizeof(void*) байт. Поэтому приведенное выше утверждение неверно.

можно использовать смежные места хранения, так как их размер известен и равен друг другу, когда их тип определен.

Это неясно. Если вы говорите о хранилище, поддерживаемом контейнером, то да, хранилище, поддерживаемое std::vector, гарантированно будет смежным.

+0

К сожалению, я имею в виду vptr, а не vtable. О std :: vector , он гарантированно будет смежным только с указателем на базовый класс, а не с объектами. – montefuscolo

+0

@montefuscolo: 'vptr' или' vtable', это не имеет значения. Вы храните указатели на объекты, а не 'vptr'. – Nawaz

+0

Хорошо, но каждый объект имеет 'vtpr', и этот' vtpr' одинаковый для всех объектов, поскольку все объекты одного типа. Итак, зачем хранить это 'vtpr' несколько раз? есть ли способ преодолеть это? – montefuscolo

3

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

class ShapeContainer { 
/* Virtual base */ 
}; 

class CircleContainer : public ShapeContainer { 
/* ... */ 
private: 
    std::vector<Circle> impl_; 
} 

class ShapeContainerFactory { 
/* Factory for ShapeContainer derived objects */ 
}; 

int main() { 
    ShapeContainer& collection = ShapeContainerFactory.create("Circle"); 
    collection.push_back(Circle()); 
}; 

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

+0

Это не решает много, на самом деле. * У каждого объекта * все еще есть встроенный в него vptr ... – StoryTeller

+1

@StoryTeller В этом смысл предпосылки «Чтобы указать точку 2 вашего вопроса ...» – Massimiliano

+0

@StoryTeller Да, но vptr - это просто указатель, если только ваш объекты очень легкие, это не будет иметь большого значения. – curiousguy