0

Please consider the following code:

Base b;
if (something)
    b = DerivedA();
else
    b = DerivedB();

It's well known that in such a case, 'slicing' occurs: In C++ we can't assign a variable of a base type an object of the derived type; the object would be 'sliced' off anything that isn't defined in the base type. (If we want to do such a thing, we have to use pointers or references).

I want to understand the actual reason for this. I.e., the reason why a Base variable can't hold a Derived object without slicing it.

My assumption is that the reason for this is that a Base object and a Derived object might not be of the same size, thus we can't make guarantees on being able to store an entire Derived object in a Base variable. A Base might take up 4 bytes, while a Derived is 7 bytes. So we settle to always slicing the derived object to fit the size of the base type.

We are able to do this with pointers, because they all occupy the same amount of memory.

Is this assumption correct? If not, what is the actual reason for slicing?

Artjom B.
  • 61,146
  • 24
  • 125
  • 222
Aviv Cohn
  • 15,543
  • 25
  • 68
  • 131
  • 1
    You answer your own question. 7 bytes can't go into 4. – M.M Oct 16 '14 at 22:04
  • @MattMcNabb Okay, that was my assumption, wanted to make sure. Thought maybe it was more of a design issue. BTW why can't we resize the variable to fit the assigned object? – Aviv Cohn Oct 16 '14 at 22:04
  • 1
    That is exactly what slicing does. It's just not resizing in the direction you want. – Mad Physicist Oct 16 '14 at 22:06
  • "We are able to do this with pointers, because they all occupy the same amount of memory." - that doesn't make any sense. Pointers don't necessarily take up the same amount of memory, but that is moot as pointers are not class types , so they can't be sliced anyway. – M.M Oct 16 '14 at 22:06
  • @MattMcNabb. It makes perfect sense and is true: `sizeof(Base *) == sizeof(DerivedA *)`. – Mad Physicist Oct 16 '14 at 22:07
  • @MattMcNabb In a 32 bit system, all pointers are 32 bit in size. Am I wrong? – Aviv Cohn Oct 16 '14 at 22:10
  • @MadPhysicist the C++ standard doesn't require that equality to hold, and it is irrelevant to the issue of slicing anyway. – M.M Oct 16 '14 at 22:14
  • @AvivCohn IDK, I haven't taken a survey of all extant and potential 32-bit systems . If your understanding depends on doing that then maybe you're not looking at it the right way. Anyway, one exception that comes to mind is programming with small memory model on an 80386. It's a 32bit CPU but had 16bit pointers. – M.M Oct 16 '14 at 22:15
  • I don't think size is the only reason for slicing to occur. It's a little hidden, because there are standard copy constructors and assignment operators are generated by the compiler. But imagine you would replicate the standard behavior yourself: They are only aware of your Base class's data/function members. So they cannot copy over the information added in the derived class --> slicing! With pointers, nothing is actually copied. EDIT: Oh, sorry, just saw that misberner already posted an answer like that. – Oguk Oct 16 '14 at 22:59

2 Answers2

2

The issue is with copy and move semantics (copy constructor, copy assignment, etc). You are getting a copy of the elements, however not all elements will be copied. If you have a base pointer, there will be no issue.

If you had a fully populated DerivedA object, and assigned that to a local stack base type, a copy assignment will be used, and any derived element values will be discarded.

Consider when you write your copy constructor. Do you do any additional work besides the members of the current class? How would you know what derived from the current class, and what work to do? Attempting to do so would be very bad.

class BaseType
{
private:
    int m_i;

public:
    explicit BaseType(BaseType const & other) // copy ctor
    {
        m_i = other.m_i;   // bitwise copy or memberwise copy will suffer the same issue
        // what else is there to do?  
        // BaseType has no knowledge of any other members
    }

    BaseType & BaseType::operator=(BaseType const & other)  // copy assignment
    {
        m_i = other.m_i;
        // what else is there to do?  
        // BaseType has no knowledge of any other members
    }
};

Even with a bitwise copy (for std::is_trivially_copyable<T>), the size will be of BaseType, and as you point out, smaller than necessary, and will truncate data.

Hope this helps.

Jeff
  • 2,495
  • 18
  • 38
  • Yeah, you just described slicing. I'm asking the technical reason why it occurs :) – Aviv Cohn Oct 16 '14 at 22:03
  • 1
    @AvivCohn That is the technical reason. The assignment operator(or copy constructor if your example was a tad different) of Base doesn't know about Derived, nor that it should somehow copy members of Derived into itself or otherwise start behaving as a Derived. – nos Oct 16 '14 at 22:07
  • So what is more accurate: "slicing occurs because the size of the defined variable is set and can't be changed, so trying to put in it anything larger than it's size has to result in slicing"? Or: "the copy constructor of class `A` takes an argument of type `A`, so it simply ignores anything not in type `A`"? – Aviv Cohn Oct 16 '14 at 22:13
  • @AvivCohn - Yes, but consider the example above. How would you call a derived copy ctor, or how would you get it to somehow get filled up? a virt function? – Jeff Oct 16 '14 at 22:16
  • 1
    @AvivCohn The latter. You could write a copy constructor/assignment operator in Base that's overloaded on Derived which dynamically allocate space for a copy of Derived, and alter the behavior of all functions in Base to delegate calls to the allocated copy of Derived based on whether a Derived has been copied in. But that would be madness on an unprecedented level, here's an example: http://ideone.com/16hLr6 – nos Oct 16 '14 at 22:33
  • @Aviv They are both equally accurate and describe two separate problems relating to slicing. One is strictly practical; the other is more about semantics but has obvious practical ramifications. – Lightness Races in Orbit Oct 17 '14 at 00:00
  • @AvivCohn - Both are true. Bitwise copy or memberwise copy will suffer the same issue. – Jeff Oct 17 '14 at 00:02
2

No. In your example, the reason for slicing is different.

In the line Base b;, you allocate space for an object of type Base on the stack and already invoke its default constructor. Therefore, in each of the branches of your if-statement, what happens is an assignment to b, which is realized through an assignment operator, usually with signature Base::operator=(const Base&). If you don't overload this operator, its default semantics are a field-by-field copy. Note that the argument type is Base (or const Base&), so only the Base-fields of the right-hand side are visible!

Assuming you had some way of storing all the information contained in a DerivedA object in a Base object (though this is unlikely), you could overload your assignment operator as Base::operator=(const DerivedA&), implement your own assignment semantics, and the above would work perfectly fine.

misberner
  • 3,488
  • 20
  • 17
  • I think that counts as slicing (it could be done either by copy-assignment operator or by copy-constructor) – M.M Oct 16 '14 at 22:08
  • Yes, but OP wrote "So we settle to always slicing the derived object to fit the size of the base type.", while the observed effect has nothing to do with automatic size truncation or the like, but merely with the assignment `operator=` which has not been overloaded to address the characteristics of a `DerivedA` instance (as I said, it theoretically could). My understanding was that the question was not about whether this is slicing, but what the reasons for slicing are. – misberner Oct 16 '14 at 22:15