views:

379

answers:

7
#include<stdio.h>

class A { public: int a;};

class B: public A {
    int c; 
    int d;
};

int main() {

    A* pA = new B[10];
    B* pB = new B[10];

    printf("\n%d", pA->a);
    pA++;
    printf("\n%d", pA->a);  // prints junk value

    printf("\n\n%d", pB->a);
    pB++;
    printf("\n%d", pB->a);
    return 0;
}

The second printf prints a junk value.

It should figure that it is pointing to an object of type B and increment by the sizof(B).

Why does that not happen?

+7  A: 

It can only know that at runtime. Imagine it slightly changed

A* a;
if(runtimevalue)
  a = new A[10];
else
  a = new B[10];

But that's not going to happen. C++ puts emphasize in speed, but this would basically make it into a language that ensures safety of operations. There is Java, C# and others that already solve this.

Kernel and device driver developers don't want a clever language runtime. They just want to have things run fast.

Have a look at Common undefined behavior in C++ question for all the things that will need to get "fixed" along. It won't be C++ anymore!

Johannes Schaub - litb
It's not purely about speed either -- if I'm writing (for example) an arbitrary looking set of bits to create a structure for an MMU, things will break if the run-time suddenly decides react on the assumption that the bit-pattern I've written means the memory really contains some other type than what I've said it does.
Jerry Coffin
@Jerry yes that in addition. Good point.
Johannes Schaub - litb
Whilst what you say is true about runtime behaviour, the example in the OP could have been caught at compile time if the type returned by `new[]` wasn't a pointer to an object, but a pointer to an array of objects. The same goes for the compiler tracking use of `delete[]` on pointers to arrays. Both are completely checkable at compile time, if you are willing to trade away backwards compatibility with C's degrading of arrays to pointers.
Pete Kirkham
@Pete Kirkham: I agree, but that would only be possible with arrays of compile-time size.
AndreyT
"if you are willing to trade away backwards compatibility with C's degrading of arrays to pointers" - and introduce a variable-length-array type. Which is doable (C99 did it, to some extent), but isn't as straightforward as you'd hope, since it requires special-casing various parts of the standard. For example, "`sizeof` is evaluated at compile time (except for variable-length arrays)".
Steve Jessop
@AndreyT you could evaluate `new T[n]` to `T(*)[]`, but i think this would cause more trouble than it helps to avoid.
Johannes Schaub - litb
+20  A: 

No it shouldn't. The declared type of pA is A*, so it is incremented by sizeof(A), which leaves it pointing to the middle of the first B in the array.

Stephen C. Steel
+1  A: 

You're incremeneting the variable a, which is a locally declared pointer of A objects. That's the same as saying a=a+sizeof(A).

Since sizeof(B)>sizeof(A), you end up pointing into the middle of the first object. When C++ then adds the appropriate offset, it'll end up reading the c field of the first B object. That happens to be unitialized memory, containing "junk".

Michael Madsen
i think you mean sizeof(B) > sizeof(A)
Marlon
@Marlon: Yes, of course. Thanks for noticing.
Michael Madsen
+11  A: 

The reason it's fragile is that you're side-stepping everything it does to try to keep you safe. Until you're sufficiently experienced to know why these problems are arising, and how to avoid them, you should:

  1. Forget that printf exists. Use std::cout instead.
  2. Forget that new exists. Use std::vector instead.

You should probably also read the C++ FAQ, and pay close attention to the part that says something to the effect that: "Even if an X is a Y, an array of X is not an array of Y."

Edit: As to why you're seeing the behavior you are, it's pretty simple: pointer arithmetic is defined in terms of the static type, not the dynamic type. That means it's based entirely on the type of pointee you defined for the pointer, NOT what it's pointing at. If you say it's pointing at an A, but then point it at a B, the arithmetic will still be done as if it was pointing at an A as you said it would.

Jerry Coffin
Using a vector instead of an array would prevent him from doing the invalid cast, but using output streams wouldn't be any safer than printf here. Using standard library is a components is a compliment, not a substitute, for understanding the basics of the language.
Alan
@Alan: while it's true that `printf` didn't cause this particular problem, it's still rarely a good thing to use in C++. I don't advocate anything as a substitute for understanding, but I do advocate following reasonable rules until you do understand well enough to know when to break them.
Jerry Coffin
in response to your part 2 -> why doesn't there exist an auto_ptr<> style RAII idiom where you simply have { new_ptr<Type> obj; Type* t1 = obj.get(); } I really don't want vector semantics for a single pointer value.
Chris Kaminski
@Chris: there are a couple of things like that (including `auto_ptr`, `unique_ptr`, and `shared_ptr`). The number stems (mostly) from different ideas of what such a thing should really do. Most of the time, you're better off just using an object directly though.
Jerry Coffin
@Jerry Coffin: what I mean is that want something like this: *new_ptr<Type> obj;* instead of *auto_ptr<Type> ptr(new Type);*. I don't EVER want to use operator new directly.
Chris Kaminski
+1  A: 

the objects have no record of what or how large they are, it's just allocated memory. The only way the compiler knows how to treat the object at the pointer memory, is by looking at the pointer type. So based on pointer A*, it will only assume an object of sizeof(A).

catchmeifyoutry
+3  A: 

Pointer a points to an object that has static type A and dynamic type B. Pointer arithmetic in C++ works in terms of static type. So, from the point of view of pointer arithmetic, a points to an object of type A.

AndreyT
+1  A: 

For reasons of compatibility with C arrays degrade to pointers. The type of B[10] may degrade to B*, and inheritance means that the assignment of a pointer to B to a variable of type A* is valid.

You then increment the value of this pointer, which adds the size of A to its address.

However, your assumption that incrementing a pointer is a valid operation if the pointer is not pointing to an array of elements of the type of the pointer is not correct.

If you try and combine the parts of C++ which are there so it behaves like C with the more strongly typed OO features, the looser typing in the C parts defeats the stronger typing in the C++ parts. It's best to keep them separate, or at least document the expected behaviours.

Pete Kirkham