1

Given this code:

#include <iostream>

class base {
private:
    char x;
public:
    base(char x) : x(x) {
        std::cout << "base::base(char) " << this << std::endl;
    }
    base(base&& rhs) {
        std::cout << "base::base(base&&), moving from " << &rhs << " to " << this << std::endl;
        x = rhs.x; rhs.x = '\0';
    }
    virtual ~base() {
        std::cout << "base::~base " << this << std::endl;
    }
};

class child : public base {
private:
    char y;
public:
    child(char x, char y) : base(x), y(y) {
        std::cout << "child::child(char, char) " << this << std::endl;
    }
    child(child&& rhs) : base(std::move(rhs)) {
        std::cout << "child::child(child&&), moving from " << &rhs << " to " << this << std::endl;
        y = rhs.y; rhs.y = '\0';
    }
    virtual ~child() {
        std::cout << "child::~child " << this << std::endl;
    }
};

int main(int argc, char* argv[]) {
    { // This block enables me to read destructor calls on the console...
        base o = child('a', 'b');
    }
    std::cin.get();
    return 0;
}

In the stack frame of main there's an area where child is being instantiated. After that the move constructor of base is being called with a reference to the newly created child. The (move) construction of base takes place in a different area in the same stack frame and copies the parts of child which are derived from base. From now on all that's left of child is the base parts, making it nothing more than base.

I know that polymorphism is not possible for objects on the stack and I think I understand why: Efficiency. No vtable lookups and such. The compiler can choose the methods to call at compile time.

What I don't understand is: Why is it possible to assign a child instance to a base variable (in a stack frame) when everything that makes up child gets lost? What's the motivation for that? I'd expect a compile error or at least a warning, because I can't think of a good reason to allow it.

Brian
  • 872
  • 8
  • 16
  • 5
    "*polymorphism is not possible for objects on the stack*" - yes it is. Polymorphism is not dependent on whether the object is allocated on the stack or the heap, only that the object is accessed via a pointer or reference after being allocated (otherwise [object slicing](http://en.wikipedia.org/wiki/Object_slicing) occurs, such as in your example). – Remy Lebeau Mar 26 '15 at 21:53
  • "I can't think of a good reason to allow it.". Stroustrup couldn't think of a good reason to *dis*-allow it. Compiler semantic errors are there to protect the language from itself, not the programmer from himself. – user207421 Mar 26 '15 at 21:54
  • Creating an instance of a base class from an instance of a derived class might make sense for some applications and might not make sense for other applications. If it does not make sense for your application, you can prevent it by making the destructor of the base class a `protected` member function or by making it pure `virtual`. – R Sahu Mar 26 '15 at 21:56
  • 1
    One reason I can think of is that it is possible the `child` constructor will construct the object differently than the `base` constructor would. Therefore if you instantiate using the `child` constructor even if the subsequent object is then treated as just an instance of `base`, it could still be different than if you had instantiated using the `base` constructor. – Reximus Mar 26 '15 at 21:57
  • @RemyLebeau Thanks for the correction regarding polymorphism on the stack. – Brian Mar 26 '15 at 22:09
  • @RSahu Good point and thanks for the hint on the destructor. I'll check it out later. – Brian Mar 26 '15 at 22:11
  • mostly duplicate of [What is slicing?](http://stackoverflow.com/questions/274626/what-is-object-slicing) – M.M Mar 26 '15 at 22:50
  • @MattMcNabb Thanks for sharing this information. At least now I know what it's called. I wouldn't call this question a duplicate of the mentioned one though, because here I'm asking for the motivation for enabling it. – Brian Mar 27 '15 at 07:49
  • @Brian well, Derived objects can be used where reference/pointer to Base is expected; and objects can be cloned via their copy-constructor. To disallow this you'd have to disallow one of those. – M.M Mar 27 '15 at 10:14

2 Answers2

0

Keep in mind that base o = child('a', 'b') is actually invoking the copy constructor, so you're not really ruining your child object, but rather, constructing o from child as a copy. You need to use pointers/references for polymorphic access to one underlying object.

child c = child('a', 'b');
base o = c; // at this point we have two objects. 

Other languages, such as Java and C# have similar looking syntax for handling objects, but they actually uses references and is therefore completely different than what we are doing here.

Also, in some cases, the base and subclass have the same size anyway. One motivating case for this is a class that is just a wrapper for an int which exposes members that do bit manipulation to pack stuff inside. Since the object itself is small, moving it around by value makes sense. But since you might just want to view the raw int, it can make sense to downcast it to a more primitive type and nothing would be lost.

EDIT: Okay, it turns out the move constructor SHOULD be invoked, but Visual Studio (my compiler) does not automatically generate move constructors yet.

#include <iostream>
class Base
{
public:
    Base() {}
    Base(const Base& other) { std::cout << "Copy"; }
};

class Der : public Base{};

void main() {
    Base b = Der();
}

Which outputs "Copy" in Visual Studio. But if you add your own move constructor, that is invoked instead because Der() is an r-value.

Keep in mind that move constructors are a new addition to C++, so prior to 11, the syntax was allowed. Since they have to support legacy C++ syntax, they would not really be able to block this from compiling just because it doesn't always make as much sense since move constructors were added. This might be the real answer to the question: legacy, because slicing in the auto-generated move constructor is more debatable.

VoidStar
  • 5,241
  • 1
  • 31
  • 45
  • I think in case of `base o = child('a', 'b');` it's not the copy constructor that's being invoked, but the move constructor (due to the fact that `child('a', 'b')` is an rvalue). In case of `base o = c;` you're right, that'd call the the copy constructor. Please correct me if I'm mistaken. Btw: Also a very interesting motivation. I'll consider it to be the answer to my question. – Brian Mar 26 '15 at 22:29
  • Had to double-update my answer, yes it should be the move constructor if it exists, since it's an r-value, but at least with visual studio you have to manually define a move constructor for that to happen (it doesn't automatically generate move constructors like the standard requires). If you disallow the move constructor, it would fall back to the copy constructor. – VoidStar Mar 26 '15 at 22:47
0

This is getting too long for a comment, and is part of an answer really:

I believe by now it is clear that you are actually creating a new base object, not assigning some existing value to a variable (since we're not in Java here; the C++ code to get Java behavior would be something like std::shared_ptr<base> o = std::make_shared<child>('a','b');). Also according to the comments, you are now looking for the motivation of why that has been allowed. The answer is really straightforward:

Because child publicly derives from base, also known as having an “is-a” relationship, any child object, in a very literal sense, is a (special kind of) base object. In a well-designed class hierarchy, you can use a child object in any place a base object is expected (and it will honor the interface invariants). This is an important concept, which is why it has a name: The Liskov Substitution Principle. (It's also why the examples you find in dusty old books on class design that had Rectangle derive from Shape and Square derive from Rectangle were really bad examples: In software design, “is a” means/should mean something different than it does in mathematics.)

Now, you can move-construct a base object from a base object, yes? Since a child object is a base object, it follows that you can also move-construct a base from that child. (Unless you break the contract established by publicly deriving from base, of course. C++ gives you a machine gun and duct tape to obliterate your foot, if you so desire.)

Christopher Creutzig
  • 8,656
  • 35
  • 45
  • The "is-a" relationship is a satisfying answer to me. And I also think that I'll try to not do things like `base o = child();`. :) Thanks. – Brian Mar 27 '15 at 18:30