Fog Creek Software
Discussion Board




C++: conversion from derived to base

My apologies if this is a silly subject.

I'm reading about the "slicing" of derived objects to their base classes. The book says: Such "sliced" objects *are* base class objects ..... and **when virtual functions are called on them, they resolve to virtual functions of the base class**.

I wrote this to see what happens:

class Base {
protected:
    int x;
public:
    Base(int xx=0): x(xx) {}
    virtual void print() { cout << "Base.x = " << x << endl; }
};    

class Derived: public Base {
public:
    Derived(int xx=0): Base(xx) {}
    void print() { cout << "Derived.x = " << x << endl; }
};

And sure enough, when I pass a Derived object to a function that expects a Base, the Base::print() is called.

Conceptually, this is right. But down at byte level, it isn't all that straight: both Base and Derived are 8 bytes long (the vfptr pointer and the int). So it's not like you just take the first N bytes from a Derived object and you've got yourself a Base object (which the word "slicing" suggests).

This isn't "slicing" as much as it is "fiddling": the compiler sees you're passing a Derived to a function that expects a Base, and mucks with the binary representation to construct a Base argument.

So with this function:

void do_it(Base b)
{
    b.print();
}

When you call it:

Derived d;
do_it(d);

Does the compiler do a cast, or what?

So, casting from a derived to a base means fiddling around with the binary representation?

Alex
Saturday, May 15, 2004

"So it's not like you just take the first N bytes from a Derived object and you've got yourself a Base object (which the word "slicing" suggests)."

Yes, it is like that (with single inheritance anyway) but in this case N is equal to 8 since Derived doesn't introduce any new fields.

Frederik Slijkerman
Saturday, May 15, 2004

"Does the compiler do a cast, or what?"

Since you're passing by value, the compiler makes a copy of just the base part of the derived object and passes that to the function expecting a base.  They are no longer the same instance.

Doug
Saturday, May 15, 2004

I don't believe C# behaves in this manner, does it?

Weee
Saturday, May 15, 2004

No, C# doesn't behave in the same manner, because you can't pass a class type object by value, only by reference (or by reference to the reference).

Brad Wilson (dotnetguy.techieswithcats.com)
Saturday, May 15, 2004

>> the compiler makes a copy of just the base part

Structure of Base:
vfptr: 4 bytes
int: 4 bytes

Structure of Derived:
vfptr: 4 bytes  <---- a *different* one
int: 4 bytes

what exactly is "just the base part"?

Alex
Saturday, May 15, 2004

Instances of Derived must have access to *two* tables of functions: one to methods of Base, one to methods of Derived. This is because the implementation of Derived must be able to call methods of Base.

The "slice" operation forgets about the Derived function table.

Think about it
Saturday, May 15, 2004

Are you sure you want to pass that class by value?
If you had passed a reference or ptr, you'd be ok.

B
Saturday, May 15, 2004

> This is because the implementation of Derived must be able to call methods of Base.

When a Derived method calls a Base method it does this using static linking (not by using the vtbl of the Base class).

> Does the compiler do a cast, or what?

It creates a copy: something like "Base temp(d); do_it(temp);"

Christopher Wells
Saturday, May 15, 2004

Just to make it clear, to avoid the slicing problem, you should pass a a Base object reference to do_it():

void do_it(Base& b)
{
    b.print();
}

Now when do_it() is given a Derived object, it will now call Derived's print() member function.  If you want to guarantee to callers of do_it() that you won't mess with "b," make sure you constify the reference passed in:

void do_it(const Base& b) // Behaves must like call-by-value
{
    b.print();
}

Antonio Rossellino
Saturday, May 15, 2004

Christopher:

>> Base temp(d);

An implicit "copy constructor"?

Base(const Derived&);

Now that's weird. What's weirder, I overloaded it, and it gets called!

The fun...

Alex
Sunday, May 16, 2004

> An implicit "copy constructor"?

Yes.

> Base(const Derived&);

No: if you haven't defined a copy constructor in the Base class, then the compiler will generate one whose signature is "Base(const Base&);" ... this is the copy constructor that "Base temp(d);" is using ... it's allowed to use it because d isa Base (it's a down-cast).

> What's weirder, I overloaded it, and it gets called!

When you defined a "Base(const Derived&);" constructor, then your constructor was called instead of the compiler-generated "Base(const Base&);" copy-constructor being called (because, following the usual rules for overloaded methods, your constructor's signature is a better fit than the copy-constructor's, for a parameter of a type Derived).

Christopher Wells
Sunday, May 16, 2004

Curiously, I defined it so:

void Base(const Derived&) { cout << "my special copy constructor\n"; }

i.e. copies no data members, returns nothing.

It does get called, but still, the Base object gets constructed correctly.

It's not really important. I'm mucking around too much.

Alex
Sunday, May 16, 2004

I think most people have cleared it up for you, but:

It not idiomatic C++ to rely on slicing, or to even use it.  It only happens when you try to squeeze an instance of Derived into a instance of type Base.  Pretty much that happens when you're passing a Derived into a function which accepts a Base (not Base& or Base*).

It is not common to ever want to do that.  It is more idiomatic to pass a reference, as people said, which avoids the problem altogether.

Slicing is pretty much the only thing a compiler could do in that instance... you could argue that it shouldn't be part of the language.

Roose
Sunday, May 16, 2004

Of course.

My observation was that, although "splicing" is bad, most likely unwanted behavior, there is explicit provision in the compiler to perform it.

I.e. whenever there is a type cast, it takes the steps needed to do the cast;

Whenever there is a splicing scenario, it takes specific *nontrivial* steps to do the splicing.

Alex
Monday, May 17, 2004

I think of it as making a copy, not as casting a type ... and making a copy is simply usual behaviour, whenever a parameter is pass-by-value.

Christopher Wells
Monday, May 17, 2004

*  Recent Topics

*  Fog Creek Home