Iterate through objects with a common base class in conditional memory

I am trying to figure out how I can iterate through a container (e.g. std :: vector) of objects that share the main parent class contiguously .

Use the following examples to demonstrate the problem.

class Base { public: Base(); virtual void doStuff() = 0; }; class DerivedA : public Base { private: //specific A member variables public: DerivedA(); virtual void doStuff(); }; class DerivedB : public Base { private: //specific B member variables public: DerivedB(); virtual void doStuff(); }; 

Now, using std :: vector for iteration, the object will be stored in adjacent memory, but we will experience slicing, since there is no room for derived properties.

Therefore, we must use a polymorphic technique using pointers such as

 int main () { std::vector<Base*> container; container.push_back(new DerivedA()); container.push_back(new DerivedB()); for (std::vector<Base*>::iterator i = container.begin(); i!=container.end(); i++) { (*(*i)).doStuff(); } } 

As far as I know, this should work fine, given that the classes are implemented.

Problem: Now the vector contains pointers in contiguous memory, but this does not mean that the addresses to which they point are.

Therefore, if I want to delete and insert objects into the vector on the fly at any time, the objects will be distributed throughout the memory location.

Question: It seems that everyone suggests doing this std :: vector, but why is it not considered problematic that it does not repeat in memory (provided that we really use a pointer)?

Am I forced to do this with pasta copy?

 int main () { std::vector<DerivedA> containerA; DerivedA a; containerA.push_back(a); std::vector<DerivedB> containerB; DerivedB b; containerB.push_back(b); for (std::vector<DerivedA>::iterator i = containerA.begin(); i!=container.end(); i++) { (*i).doStuff(); } for (std::vector<DerivedB>::iterator i = containerB.begin(); i!=container.end(); i++) { (*i).doStuff(); } } 

I assume that there cannot be a real solution to this, since storing objects of various sizes in a linear order in memory does not make sense, but if anyone could give me some advice, I would appreciate it.

+10
c ++ vector
source share
6 answers

Let's look at the questions in order.

Q: How can I create a continuous heterogeneous container?

A: You cannot.

Suppose you used some placement of a new fraud to arrange your objects in memory like this:

  [B ][DA ][DB ][B ][B ][DB ][DA ] 

How does the iteration mechanism know how far the iteration of the pointer will go from one object to another? The number of bytes from the first element to the second differs from the second to the third.

The reason why adjacent arrays must be uniform is because the distance from one object to the next is the same for all elements. You could try inserting a size into each object, but then you have basically a linked list, not an array (although one with a good location ).

This reasoning leads to the idea of ​​using an array of pointers about which you asked the following question:

Q: Why is it not considered problematic that it is not continuous continuously

A: It is not as slow as you think.

It seems your concern is to follow the following pointers to scattered memory locations. But the cost of following these signs is unlikely to be dominant. Don't dwell on micro-optimizations such as memory layouts until you have strong evidence that they are needed.

Q: Am I forced to do this using carbon paste?

A: No!

Here the problem is most likely in maintainability, and not in performance. Maintainability is much more important, in my opinion, and it's good to think about the early.

For ease of maintenance, you already have a great solution: to support the Base* vector.

If you really want to use multiple vectors, there is an even better way than copy and paste: use a template function like this (not verified):

 template <class T> void doStuffToVector(std::vector<T> &vec) { for (std::vector<T>::iterator i = vec.begin(); i!=vec.end(); ++i) { (*i).doStuff(); } } 

Then call it for each container:

  doStuffToVector(containerA); doStuffToVector(containerB); 

If your only concern is maintainability, then either a vector of pointers or a template function should be enough.

Q: Any advice?

A: For starters, ignore performance , at least until constant factors are concerned. Focus on correctness and maintainability.

Then measure the performance . Note that this question did not start with the current and desired speed. You do not have an actual problem to solve!

If after a measurement you make a conclusion too slow, use the profiler to find out where the slow spots are. They are almost never where you think they will be.

Case in point: this whole question and answers focused on iteration, but no one raised the question that the virtual function calls to doStuff are likely to become a bottleneck! virtual function calls are expensive because they are an indirect control flow, which causes serious problems for the pipeline ; indirect access to data is much cheaper because the data cache is usually very efficient for quickly satisfying data access requests.

Q: (Implicit) How would I optimize this?

A: After careful measurement, you may find that this code (the iteration itself, including sending the virtual function; not that inside the doStuff ) is a bottleneck. This should mean that it runs at least billions of iterations.

First, consider algorithmic improvements that will reduce the number of iterations required.

Then exclude the call to the virtual function, for example, by embedding an explicit indicator of the type of the object and testing it with if or switch . This will allow the processor branch predictor to be much more efficient.

Finally, yes, you probably want to combine all the elements into one continuous array to improve locality and eliminate indirect access data. This will mean the elimination of the class hierarchy, so that all objects are of the same type, or the union of all fields into one class and / or using union . This will damage your maintainability program! This is sometimes one of the costs of writing high performance code, but in fact it is only very rarely needed.

+3
source share

A very simple solution is to sort the array of pointers by address value. Then, if you iterate over your vector, they will be in memory order. Perhaps not contiguously, but nonetheless in an order that reduces the number of caches.

The only way to truly have contiguous memory is to allocate it as such, for example, to have vectors of objects of a derived type stored in their own container, which you then reference in the pointer vector.

0
source share

Everyone seems to suggest doing it the way std::vector , but why isn't it considered problematic that it doesn't repeat continuously in memory (assuming we actually use a pointer)?

I do not know who considers this problematic or not. As with the other answers, in many cases you just don't care. Perform profiling and you will see if you need to optimize it or not.

In most cases, people recommend that you use std::vector<std::unique_ptr<...>> .

However, in many cases it is very important to store your objects in continuous memory. Games are one such case. I write a lot of computational code (finite element libraries), where this is also very important. You can read about how to organize your data in a different way so that everything is lined up. For example, it may be interesting to save all Arm objects in std::vector , rather than saving each Arm in a Hero object and accessing Arm objects through a Hero object.

In any case, here is an easy way to store your objects from your example in a continuous container.

For the base class use alignas to fix the size of the object. Make sure it is large enough to fit all derived objects. In my example below, DerivedA has a size of 16, DerivedB has a size of 24. The specified alignment size should be a power of 2, so we select 32.

 struct alignas(32) Base { virtual void print() const {} }; struct DerivedA : Base { void print() const final override { std::cout << "num: " << i << std::endl; } int i = 1; }; struct DerivedB : Base { void print() const final override { std::cout << "num: " << i << std::endl; } int i = 2; double j = 100.0; }; 

Now we can write instances of DerivedA and DerivedB using placement new :

 int main () { std::vector<Base> v(2); new (&v[0]) DerivedA(); new (&v[1]) DerivedB(); for (const auto& e : v) e.print(); return 0; } 

EDIT

The problem here is that you need to manage the dimensions manually. Also, as I recently pointed out, alignas intended to position an object in memory, and not to allocate size. Perhaps the best way would be to simply use std::variant .

 int main() { std::vector<std::variant<DerivedA, DerivedB>> vec; vec.emplace_back(DerivedA()); vec.emplace_back(DerivedB()); for (const auto& e : vec) std::visit(VisitPackage(), e); return 0; } 

where VisitPackage might look something like this:

 struct VisitPackage { void operator()(const DerivedA& d) { d.print(); } void operator()(const DerivedB& d) { d.print(); } }; 

Below is a complete and short example of how to get what you want using std::variant .

 #include <iostream> #include <vector> #include <variant> struct Base { virtual void print() const = 0; }; struct DerivedA : Base { void print() const final override { std::cout << "DerivedA\n"; } }; struct DerivedB : Base { void print() const final override { std::cout << "DerivedB\n"; } }; struct Print { template <typename T> // note that the operator() calls print from DerivedA or DerivedB directly void operator()(const T& obj) const { obj.print(); } }; int main () { using var_t = std::variant<DerivedA, DerivedB>; std::vector<var_t> vec { DerivedA(), DerivedB() }; for (auto& e : vec) std::visit(Print(), e); return 0; } 
0
source share

If we need to store objects in an array, their type must be fixed. Then we have these options:

  • dynamically place and store pointers - if the specified objects need to be continuous in memory, use a special allocator
  • use a fixed-size polymorphic type, such as union , as the storage type

For the second option, the code might look something like this:

 #include <new> struct A { A() {} virtual void f() {} }; struct B : A { B() {} void f() override {} }; union U { A a; B b; U() {} }; int main() { U u[2]; new (&u[0]) A; new (&u[1]) B; ((A*)&u[0])->f(); // A::f ((A*)&u[1])->f(); // B::f } 
-one
source share

std::vector<T> iterators assume that objects in adjacent memory are of type T , std::vector<T>::iterator::operator++ considers that sizeof T is invariant, that is, it does not access a specific instance for data size.

Essentially, you can think of vector and vector::iterator as a thin facade over the T* m_data , so iterator++ is actually just the main pointer operation.

You will most likely need to use your own distributor and in place of new to prepare your data, accompanied by indexing, linking, etc. Maybe think about http://www.boost.org/doc/libs/1_58_0/doc/html/intrusive/slist.html

See also boost :: stable_vector

-3
source share

std::vector allocates objects in continuous memory, but the pointers to the objects you store in the vector are not. So you repeat vector . The following code is written in C ++ 14. The described problem is not solvable by this solution, since object pointers are stored in continuous memory, but not in real objects.

 #include <iostream> #include <memory> #include <vector> #include <algorithm> using namespace std; class Base { public: Base() {} virtual void doStuff() = 0; }; class DerivedA : public Base { private: //specific A member variables public: DerivedA() : Base() {} virtual void doStuff() { std::cout << "Derived Class A - Do Stuff" << std::endl; } }; class DerivedB : public Base { private: //specific B member variables public: DerivedB() : Base() {} virtual void doStuff() { std::cout << "Derived Class B - Do Stuff" << std::endl; } }; int main() { // your code goes here std::vector<std::unique_ptr<Base> > container; container.push_back(std::make_unique<DerivedA>()); container.push_back(std::make_unique<DerivedB>()); std::for_each(container.begin(), container.end(),[](std::unique_ptr<Base> & b) { b->doStuff(); }); return 0; } 

Live Demo is here .

-3
source share

All Articles