Skip to main content
GameDev.net gamedev.net
🔒 Locked

Confusion with smart pointers

Started by 215648 Oct 19, 2015 at 7:08 PM 15 replies 4.2k views
Original Post
215648
215648

So, I read that you cannot have two std::unique_ptr point to same object. If you want to do that, you need to use std::move and that'll transfer ownership of it. So, I tried to do that with this code.



int main()
{
 
std::unique_ptr<Random> SP(new Random());
 
printf("SP : %X\n", SP.get());
 
std::unique_ptr<Random> SP2(std::move(SP));
 
printf("SP : %X\n", SP.get());
printf("SP2 : %X\n", SP.get());
 
SP->SomeFunc();
SP2->SomeFunc();
 
system("PAUSE>NUL");
 
return 0;
}
and got this output:
tHARAaU.png
Previously, SP was pointing to some address (A6A748) and now it points to 0 because it has transferred it's ownership to SP2 (So I assume it's nullptr?)
But the confusion I get is that this now points to nullptr (that is what I think) yet I am able to call SP->SomeFunc();.
Shouldn't I get an error because now SP doesn't point to anything cause it has moved it's ownership to SP2 ?
EDIT:
Here's the code for Random class if you're wondering what SP->SomeFunc() and SP2->SomeFunc() do


class Random
{
public:
Random();
~Random();
 
void SomeFunc();
};
 
void Random::SomeFunc()
{
printf("SomeFunc() called (Address: %X)\n", this);
}
 
Random::Random()
{
printf("Constructor called!\n");
}
 
Random::~Random()
{
printf("Destructor called!\n");
}
BitMaster
BitMaster
Calling a member function of a null-pointer is undefined behavior. You might get an error. You might get something that looks like a working program. You might get something that looks like a working program but subtly corrupts memory leading to a crash down the road with no obvious connection to the cause.

For most compilers you can call member functions on a null-pointer provided certain requirements are met (not a virtual function, does not access any member data). It's still a horribly idea. It's still undefined behavior. It could still break the next time a new compiler version comes out or you change any compiler settings.
Bregma
Bregma

Well, you're now entering the realm of undefined behaviour.

You're trying to call the member function of a pointer-to-object that has the value nullptr. Anything could happen.

A likely implementation of member function dispatch is that there is a static table of functions for a class (note the lack of virtual functions here, so there is no need for a virtual table dispatch). When a member function is called, it's a matter of calling the appropriate funciton and passing the pointer value as 'this'. I'm not saying that's the way your compiler runtime works, but it's a reasonable implementation and would give the (undefined) behaviour you're observing.

A better use of a std::unique_ptr is to design your software to avoid having to check to see if the pointer is null all the time. Otherwise, it's just like a regular pointer that gets nulled out when you transfer ownership (and everything that entails).

Stephen M. Webb
Professional Free Software Developer
phil_t
phil_t

c++ is low level - there's no fancy-shmancy runtime that would check for null before calling a method. If you look at the assembly generated for your code (always educational if you want to understand how things work under the hood), you'll see why you are getting those results.

Ravyne
Ravyne

[EDIT] Adding this note to be clear as to what the others are saying -- this is undefined behavior. However, it's likely to work as and why I describe below, and as others have alluded to because it just falls out of how a compiler does dispatch. The compiler isn't doing any special work to make this work, and doesn't care that it does. If a compiler vendor found a better way to do dispatch that broke the behavior you're seeing they would be free to do so and still maintain their standards conformance. In practice, they might choose not to do so, or might put one or the other behavior behind a compiler switch precisely because there probably exists code that relies on this behavior even though it shouldn't, strictly speaking.

In SomeFunc, you're not actually dereferencing 'this' at any point. You examine its value (to print it) and that's fine, you're allowed to look at a null pointer -- its only dereferencing it that's undefined.

Functions themselves aren't part of an object instance and so don't require a dereference to dispatch (unless they're virtual) -- they exist in your executable always (presuming they're called at least once, or you've instructed the toolchain to leave them even if not). Calling a virtual function would change that, because there's an indirection happening through a virtual function table pointer that's stored in the object (hence you'd have to dereference the object first) -- this, I believe, is only a problem when the the virtual member function is called through a pointer to base class though; if the compiler can statically know which virtual function to call I'd imagine it elides the indirection (but I'm not 100% certain).

Also:

printf("SP : %X\n", SP.get());
printf("SP2 : %X\n", SP.get()); <-- You're not printing SP2.get() here.
throw table_exception("(? ???)? ? ???");
NightCreature83
NightCreature83

You would get access violations if you were trying to access member variables in those functions on very low address. Like access violation at 0x00000008 for example, usually when seeing these low address values in access violations it indicates that your object access has been cleared. Because members are usually accessed by adding the field offset to the object memory address.

Zipster
Zipster

So, I read that you cannot have two std::unique_ptr point to same object.

Technically you can:


#include <memory>

int main()
{
   int* foo = new int;
   std::unique_ptr<int> fooPtr1(foo);
   std::unique_ptr<int> fooPtr2(foo); // Double-delete bug!
}

So always be careful (and consistent!) with how you mix and match your smart pointers with your regular pointers!

215648
215648

[EDIT] Adding this note to be clear as to what the others are saying -- this is undefined behavior. However, it's likely to work as and why I describe below, and as others have alluded to because it just falls out of how a compiler does dispatch. The compiler isn't doing any special work to make this work, and doesn't care that it does. If a compiler vendor found a better way to do dispatch that broke the behavior you're seeing they would be free to do so and still maintain their standards conformance. In practice, they might choose not to do so, or might put one or the other behavior behind a compiler switch precisely because there probably exists code that relies on this behavior even though it shouldn't, strictly speaking.

In SomeFunc, you're not actually dereferencing 'this' at any point. You examine its value (to print it) and that's fine, you're allowed to look at a null pointer -- its only dereferencing it that's undefined.

Functions themselves aren't part of an object instance and so don't require a dereference to dispatch (unless they're virtual) -- they exist in your executable always (presuming they're called at least once, or you've instructed the toolchain to leave them even if not). Calling a virtual function would change that, because there's an indirection happening through a virtual function table pointer that's stored in the object (hence you'd have to dereference the object first) -- this, I believe, is only a problem when the the virtual member function is called through a pointer to base class though; if the compiler can statically know which virtual function to call I'd imagine it elides the indirection (but I'm not 100% certain).

Also:

printf("SP : %X\n", SP.get());
printf("SP2 : %X\n", SP.get()); <-- You're not printing SP2.get() here.

Nice catch. I fixed it and this is the new output. (Still reading your people's posts, will reply when I have questions again.)
vUeZtvj.png

EDIT: There's something going on.

After using *this instead of this here,


void Random::SomeFunc()
{
printf("SomeFunc() called (Address: %X)\n", *this);
}

The program crashes at SP->SomeFunc(); but not at SP2->SomeFunc(); !

So, this works fine


 
int main()
{
std::unique_ptr<Random> SP(new Random());
 
printf("SP : %X\n", SP.get());
 
std::unique_ptr<Random> SP2(std::move(SP));
 
printf("SP : %X\n", SP.get());
printf("SP2 : %X\n", SP2.get());
 
//SP COMMENTED OUT!
//SP->SomeFunc();
SP2->SomeFunc();
 
system("PAUSE>NUL");
 
return 0;
}

but this doesn't (Crashes at SP->SomeFunc())


int main()
{
std::unique_ptr<Random> SP(new Random());
 
printf("SP : %X\n", SP.get());
 
std::unique_ptr<Random> SP2(std::move(SP));
 
printf("SP : %X\n", SP.get());
printf("SP2 : %X\n", SP2.get());
 
SP->SomeFunc();
SP2->SomeFunc();
 
system("PAUSE>NUL");
 
return 0;
}

Here is the access violating. It says Reading 0x000000, so it means now SP points to nullptr, right?

Why didn't that happen with this pointer? Why did I have to use *this (dereference) ?

QkdZQ6f.png

Juliean
Juliean

Why didn't that happen with this pointer? Why did I have to use *this (dereference) ?

This has already been answered. Reading the value of a nullpointer is ok. Using a nullpointer, which includes dereferencing it, isn't.


int* pValue = nullptr;

print(pValue); // works: prints the value of the pointer - whether it is 0x000000, 0xEF9283 doesn't matter

print(*pValue); // fails: tries to print the value of the thing the pointer is pointing to - and 0x00000 doesn't point to anything

Doesn't matter whether its "this" or a regular pointer, whether its a class or a builtin type...

SmkViper
SmkViper
Also, please keep in mind that doing anything other than destruction with a moved-from type is undefined behavior.

As you discovered, your implementation of unique_ptr decided to set itself to null, but there is no rule that requires a moved-from unique_ptr to be null, simply that it must be destroyable. Some fancy version of the standard library may decide to set a "owned" flag to false and leave the pointer alone instead. (I have no idea why they'd do something like that, but the standard does not forbid it)

Heck, a really smart compiler may decide that once you've called std::move on an object without a destructor it can re-use the stack space for something else.


Edited: As corrected by BitMaster - valid but unspecified state for standard library objects, not undefined behavior.

I will still state that I would question any code review containing code that re-used a moved from variable, simply because of the potential confusion.
BitMaster
BitMaster
While it is true that a moved-from type does not need to be anything but destructible, types in the standard library (including std::unique_ptr) promise more unless explicitly stated otherwise (right now I could not name a type that does). I'm just going to point over to a relevant StackOverflow topic which also quotes the relevant parts of the standard. Also note that any types you want to use with the standard library (for example inside a container like std::vector) must fulfill that promise too.

Edit: That explicitly means that you are allowed to call std::unique_ptr::get() and compare the std::unique_ptr with nullptr or other pointers.
SmkViper
SmkViper

While it is true that a moved-from type does not need to be anything but destructible, types in the standard library (including std::unique_ptr) promise more unless explicitly stated otherwise (right now I could not name a type that does). I'm just going to point over to a relevant StackOverflow topic which also quotes the relevant parts of the standard. Also note that any types you want to use with the standard library (for example inside a container like std::vector) must fulfill that promise too.

Edit: That explicitly means that you are allowed to call std::unique_ptr::get() and compare the std::unique_ptr with nullptr or other pointers.


Ah - my bad. Post corrected.

I had a bad case of "I should probably avoid doing this due to pitfalls" mutating into "no one else should do this" smile.png
Khatharr
Khatharr

So, I read that you cannot have two std::unique_ptr point to same object.

Technically you can:


#include <memory>

int main()
{
   int* foo = new int;
   std::unique_ptr<int> fooPtr1(foo);
   std::unique_ptr<int> fooPtr2(foo); // Double-delete bug!
}

So always be careful (and consistent!) with how you mix and match your smart pointers with your regular pointers!

Better yet, use std::make_unique() instead of naked 'new'.


#include <iostream>
#include <memory>
#include <random>

struct Foo {
  Foo(int baz) : bar(baz) {}
  int bar;
};

int main() {
  std::mt19937 generator(std::random_device{}());
  std::uniform_int_distribution<int> distribution(0, 100);

  auto thing = std::make_unique<Foo>(distribution(generator));
  
  std::cout << "The number is: " << thing->bar << ".\n\n";
  
  system("pause > nul");
}


void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.
alh420
alh420

Here is the access violating. It says Reading 0x000000, so it means now SP points to nullptr, right?
Why didn't that happen with this pointer? Why did I have to use *this (dereference) ?

There is a hint what is going on in the name of the error. "Access violation".

It means your program tried to access a memory address it did not have permission to access.

Just reading the pointer value never accesses the address the pointer points to, so no problem doing that.

You can also get access violations if you try to read or write addresses outside of memory you allocate.

A program can't just read and write any address at any time, it must instruct the os it needs the memory, and get an memory block allocated to it. (MMU maps some physical memory address to the virtual address space of the program)

Address 0 will never be mapped to a physical memory block, and will always give access violation.

iMalc
iMalc

As you are no doubt aware, in C++ it is preferable to avoid new and delete in your program. Modern C++ provides some nicer ways to achieve this with unique_ptr:

Instead of this:


std::unique_ptr<Random> SP(new Random());

Use this:


auto SP = std::make_unique<Random>();

I've almost never called std::move on a unique_ptr myself; that should be mostly called by container classes e.g. vector in a normal program, and I you wouldn't often be writing a container.

In a normal program if you have to transfer ownership between unique_ptrs, you'll often find yourself doing so by detaching from one unique_ptr, and attaching to another, most likely across a function call boundary.

phil_t
phil_t




I've almost never called std::move on a unique_ptr myself; that should be mostly called by container classes e.g. vector in a normal program, and I you wouldn't often be writing a container.
In a normal program if you have to transfer ownership between unique_ptrs, you'll often find yourself doing so by detaching from one unique_ptr, and attaching to another, most likely across a function call boundary.

That's exactly when std::move should be used.

iMalc
iMalc

I've almost never called std::move on a unique_ptr myself; that should be mostly called by container classes e.g. vector in a normal program, and I you wouldn't often be writing a container.
In a normal program if you have to transfer ownership between unique_ptrs, you'll often find yourself doing so by detaching from one unique_ptr, and attaching to another, most likely across a function call boundary.

That's exactly when std::move should be used.

Well sure, if the function signature takes a unique_ptr directly as one of the arguments. Kinda forgot that obvious case as I've almost never been in that situation. I deal mostly with code where the interface in the middle is a C interface, e.g COM, and that sort of thing.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.