Avoiding object slicing

雨燕双飞 提交于 2020-06-11 14:25:35

问题


So I am refreshing on C++, and honestly it's been awhile. I made a console pong game as a sort of refresher task and got some input on using polymorphism for my classes to derive from a base "GameObject" (that has some base methods for drawing objects to the screen).

One of the pieces of input was (and I had subsequently asked about) was how memory worked when deriving from base classes. Since I hadn't really done much advanced C++.

For instance lets say we have a base class, for now it just has a "draw" method (Btw why do we need to say virtual for it?), since all other derived objects really only share one common method, and that's being drawn:

class GameObject
{
public:

    virtual void Draw( ) = 0;
};

we also have a ball class for instance:

class Ball : public GameObject

The input I received is that in a proper game these would probably be kept in some sort of vector of GameObject pointers. Something like this: std::vector<GameObject*> _gameObjects;

(So a vector of pointers to GameObjects) (BTW Why would we use pointers here? why not just pure GameObjects?). We would instantiate one of these gameObjects with something like:

_gameObjects.push_back( new Ball( -1, 1, boardWidth / 2, boardHeight / 2 ); ); 

(new returns a pointer to the object correct? IIRC). From my understanding if I tried to do something like:

Ball b;
GameObject g = b;

That things would get messed up (as seen here: What is object slicing?)

However...am I not simply creating Derived objects on their own when I do the new Ball( -1, 1, boardWidth / 2, boardHeight / 2 ); or is that automatically assigning it as a GameObject too? I can't really figure out why one works and one doesn't. Does it have to do with creating an object via new vs just Ball ball for example?

Sorry if the question makes no sense, im just trying to understand how this object slicing would happen.


回答1:


The fundamental issue is copying an object (which is not an issue in languages where classes are "reference types", but in C++ the default is to pass things by value, i.e. making a copy). "Slicing" means copying the value of a bigger object (of type B, which derives from A) into a smaller object (of type A). Because A is smaller, only a partial copy is made.

When you create a container, its elements are full objects of their own. For example:

std::vector<int> v(3);  // define a vector of 3 integers
int i = 42;
v[0] = i;  // copy 42 into v[0]

v[0] is an int variable, just like i.

The same thing happens with classes:

class Base { ... };
std::vector<Base> v(3);  // instantiates 3 Base objects
Base x(42);
v[0] = x;

The last line copies the contents of the x object into the v[0] object.

If we change the type of x like this:

class Derived : public Base { ... };
std::vector<Base> v(3);
Derived x(42, "hello");
v[0] = x;

... then v[0] = x tries to copy the contents of a Derived object into a Base object. What happens in this case is that all members declared in Derived are ignored. Only the data members declared in the base class Base are copied, because that's all v[0] has room for.

What a pointer gives you is the ability to avoid copying. When you do

T x;
T *ptr = &x;

, ptr is not a copy of x, it just points to x.

Similarly, you can do

Derived obj;
Base *ptr = &obj;

&obj and ptr have different types (Derived * and Base *, respectively), but C++ allows this code anyway. Because Derived objects contain all members of Base, it's OK to let a Base pointer point at a Derived instance.

What this gives you is essentially a reduced interface to obj. When accessed through ptr, it only has the methods declared in Base. But because no copying was done, all data (including the Derived specific parts) are still there and can be used internally.

As for virtual: Normally, when you call a method foo through an object of type Base, it will invoke exactly Base::foo (i.e. the method defined in Base). This happens even if the call is made through a pointer that actually points at a derived object (as described above) with a different implementation of the method:

class Base {
    public:
    void foo() const { std::cout << "hello from Base::foo\n"; }
};

class Derived : public Base {
    public:
    void foo() const { std::cout << "hello from Derived::foo\n"; }
};

Derived obj;
Base *ptr = &obj;
obj.foo();  // calls Derived::foo
ptr->foo();  // calls Base::foo, even though ptr actually points to a Derived object

By marking foo as virtual, we force the method call to use the actual type of the object, instead of the declared type of the pointer the call is made through:

class Base {
    public:
    virtual void foo() const { std::cout << "hello from Base::foo\n"; }
};

class Derived : public Base {
    public:
    void foo() const { std::cout << "hello from Derived::foo\n"; }
};

Derived obj;
Base *ptr = &obj;
obj.foo();  // calls Derived::foo
ptr->foo();  // also calls Derived::foo

virtual has no effect on normal objects because there the declared type and the actual type are always the same. It only affects method calls made through pointers (and references) to objects, because those have the ability to refer to other objects (of potentially different types).

And that is another reason to store a collection of pointers: When you have several different subclasses of GameObject, all of which implement their own custom draw method, you want the code to pay attention to the actual types of the objects, so the right method gets called in each case. If draw weren't virtual, your code would attempt to invoke GameObject::draw, which doesn't exist. Depending on how exactly you code it, this either wouldn't compile in the first place or abort at runtime.




回答2:


Object slicing happens when you directly store objects in a container. No slicing can occur when you store pointers (or better smart pointers) to objects. So it you store a Ball in a vector<GameObject> it will be sliced, but if you store a Ball * in a vector<GameObject *>, all will be fine.




回答3:


The quick answer to your question is that object slicing is not an issue when you do _gameObjects.push_back( new Ball( ... )) because new allocates enough memory for a Ball-sized object.

Here's the explanation. Object slicing is an issue where the compiler believes an object to be smaller than it actually is. So in your code example:

Ball b;
GameObject g = b;

The compiler has reserved enough space for a GameObject named g, and yet you are trying to put a Ball (b) there. But a Ball may be bigger than a GameObject, and then data will get lost and bad stuff will likely start to happen.

However, when you do new Ball(...) or new GameObject(...), the compiler knows exactly how much space to allocate because it knows the true type of the object. Then, what you store is actually a Ball* or GameObject*. And you can safely store a Ball* in a GameObject* type because the pointers are the same size, so object slicing does not occur. The memory pointed at can be any number of different sizes, but the pointers will always be the same size.




回答4:


Btw why do we need to say virtual for it?

If you don't declare a function to be virtual, then you cannot call the function with virtual dispatch. When a function is called virtually through a pointer or reference to a base class, then the call is dispatched to an override in the most derived class (if any exist). In other words, virtual allows runtime polymorphism.

If the function is non-virtual, then the function can only be dispatched statically. When a function is called statically, the function of the compile time type is called. So, if a function is called statically though a base pointer, then the base function is called, not a derived override.

BTW Why would we use pointers here? why not just pure GameObjects?

GameObject is an abstract class, so you cannot have concrete objects of that type. Since you cannot have a concrete GameObject, you cannot have an array (nor vector) of them either. GameObject instances can only exist as a base class sub object of a derived type.

new returns a pointer to the object correct?

new creates an object in dynamic storage, and returns pointer to that object.

By the way, if you fail to call delete on the pointer before losing the pointer value, you have a memory leak. Oh, and if you attempt to delete something twice, or delete something that didn't originate from new, the behaviour of your program will be undefined. Memory allocation is difficult, and you should always use smart pointers to manage it. A vector of bare owning pointers such as in your example is a very bad idea.

Furthermore, deleting an object through a base object pointer has undefined behaviour unless the destructor of the base class is virtual. The destructor of GameObject is not virtual, so there is no way for your program to avoid either UB or memory leak. Both options are bad. Solution is to make the destructor of GameObject virtual.

Avoiding object slicing

You can avoid accidental object slicing by making the base class abstract. Since there can not be concrete instances of an abstract class, you cannot accidentally "slice off" the base of a derived object.

For example:

Ball b;
GameObject g = b;

is ill-formed because GameObject is an abstract class. The compiler might say something like this:

main.cpp: In function 'int main()':
main.cpp:16:20: error: cannot allocate an object of abstract type 'GameObject'
 GameObject g = b;
                ^
main.cpp:3:7: note:   because the following virtual functions are pure within 'GameObject':
 class GameObject
       ^~~~~~~~~~
main.cpp:7:18: note:    'virtual void GameObject::Draw()'
     virtual void Draw( ) = 0;
                  ^~~~
main.cpp:16:16: error: cannot declare variable 'g' to be of abstract type 'GameObject'
     GameObject g = b;



回答5:


I will attempt to answer the various questions you have asked, although others may have a more technical explanation in their answers.

virtual void Draw( ) = 0;

Why do we need to say virtual for it?

In simple terms, the virtual keyword tells the C++ compiler that the function can be redefined in a child class. When you go to call ball.Draw() the compiler knows that Ball::Draw() should be executed if it exists in the Ball class instead of GameObject::Draw().


std::vector<GameObject*> _gameObjects;

Why would we use pointers here?

This is a good idea because object slicing does happen when the container has to allocate space for and contain the objects themselves. Remember that a pointer is a constant size, regardless of what it points to. When you have to resize the container or move elements around, the pointers are much easier and faster to move. And you can always cast a pointer to GameObject back into a pointer to Ball if you are certain that is a valid thing to do.


new returns a pointer to the object correct?

Yes, what new is doing is constructing an instance of that class on the heap and then returning a pointer to that instance.
I strongly recommend that you learn how to use smart pointers though. These can automatically delete objects when they are no longer referenced. Kind of like what a garbage collector does in a language like Java or C#.


new Ball( -1, 1, boardWidth / 2, boardHeight / 2 );

...or is that automatically assigning it as a GameObject too?

Yes, if Ball inherits the GameObject class, then a pointer to a Ball will also be a valid pointer to a GameObject. As you'd expect, you can't access the members of Ball from a pointer to a GameObject though.


Does it have to do with creating an object via new vs just Ball ball for example?

I will explain the difference between the two ways to instantiate a Ball:

Ball ballA = Ball();
Ball* ballB = new Ball();

For ballA we are declaring that the ballA variable is an instance of Ball that will "live" in the stack memory. We use the Ball() constructor to initialize the ballA variable to an instance of a Ball. Since this is a stack variable the ballA instance will be destroyed once the program exits the scope in which it is declared.

For ballB we are declaring that the ballB variable is a pointer to an instance of Ball that will live in the heap memory. We use the new Ball() statement to first allocate heap memory for a Ball and then construct it with the Ball() constructor. Finally that new statement evaluates to a pointer which is assigned to ballB. Now when the program exits the scope where ballB is declared, the pointer is destroyed but the instance it pointed to is left on the heap. If you didn't save the value of that pointer somewhere else you will be unable to free the memory used by that Ball instance. This is why smart pointers are useful because they internally keep track of whether the instance is still referenced anywhere.




回答6:


This has to do with values.

Ball b;
GameObject g;

The value of b is the different values of it's variables.

The value of g is likewise the different values of it's variables.

When b is assigned to g, the variables of a "subobject" of b (inherited from GameObject ) are assigned to variables of g. This is slicing.

Now about the functions.

To the compiler a member function of a class is a pointer to a memory where the code of the function resides.

A non-virtual function is always a constant pointer value.

But a virtual function can have different values depending on which class they were declared in.

So to tell the compiler that it should create a placeholder for the function pointer, the keyword virtual is used.

Now back to the assignment of values.

We know assigning different types of variables to each other can cause slicing. So to solve this issue indirection is used - a pointer to the object.

A pointer always needs the same amount of strorage for any pointer of a type. And when a pointer is assigned, the underlying structure is left unchanged, only the pointer is copied which overrides the previous pointer.

When we call a virtual function on g which has been sliced, we might be calling the correct function from b, but the sliced g object doesn't have all the fields needed by the b function, so an error can occur.

But calling using a pointer to the object, the original object b is used, which has all the required fields used by the virtual function of b.



来源:https://stackoverflow.com/questions/56464702/avoiding-object-slicing

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!