1

Let's see the following code:


#include <iostream>
#include <memory>


using namespace std;

class myClass{
    public:
    int a = 15;
    myClass(int x): a(x){};
    ~myClass(){cout << "die die ";};
};

int main()
{
    myClass* c;
    
    {
    std::shared_ptr<myClass> a = std::make_shared<myClass>(16);
    cout<< a.use_count(); //1
    std::shared_ptr<myClass> b = a;
    cout<< a.use_count(); //2
    c = a.get();
    cout<< a.use_count(); // still 2
    } // die die
    
    cout << (*c).a; // 16? UB?

    return 0;
}

We can notice that an instance of myClass is created and is managed by a std::shared_ptr. The count increases when a new shared pointer points to it which is normal. IT DOES NOT increase when a raw pointer c starts pointing to it, which is also normal.

When the scope is closed at } we can notice that the destructor was called. However we still have access to c `s data. Also notice that the destructor was only called once.

Is this an undefined behavior? or was the instance copied at .get()? If it was copied what happened with the destructor? delete ing c crashes (which in general proves how bad an idea this is).

For context, I'm working on a project that contains quite old code and raw pointers were mixed with boost and std's smart pointers, so I want to understand how to deal with this cases.

Edit

What about if I create a new instance and not a pointer?


int main()
{
    myClass c(5);
    
    {
    std::shared_ptr<myClass> a = std::make_shared<myClass>(16);
    cout<< a.use_count(); //1
    std::shared_ptr<myClass> b = a;
    cout<< a.use_count(); //2
    c = *(a.get()); // This looks ugly, but am I getting a copy?
    cout<< a.use_count(); // still 2
    }// die die
    
    cout << (c).a; //16

    return 0;
}// die die

In this case the destructor was actaully called, is this a safe way to copy using .get()?

Ivan
  • 1,352
  • 2
  • 13
  • 31
  • 3
    Yes, this is UB. Once `a` and `b` go out of scope the object `c` points to goes with them, leaving you with a dangling pointer. – NathanOliver Mar 13 '23 at 18:04
  • The (numeric) value of the pointer was all that was copied. Once what it points to gets destroyed is _undefined behavoir_ to dereference it. – Mike Vine Mar 13 '23 at 18:04
  • [std::shared_ptr::get](https://en.cppreference.com/w/cpp/memory/shared_ptr/get) _"...returns The stored pointer...."_ that's it nothing else:- no copying of the managed object; no changing of the reference count. – Richard Critten Mar 13 '23 at 18:07
  • @NathanOliver just to make it ultre clear, I added an example to my question. I guess it's still UB – Ivan Mar 13 '23 at 18:09
  • 1
    `c = *(a.get());` gets the value of the managed pointer, dereferences it, and then calls the defaulted assignment operator for `myClass` making a (shallow) copy in `c` of the managed object. – Richard Critten Mar 13 '23 at 18:10
  • 3
    Your updated example is not UB. `c` as an object exists until the end of `main` and `c = *(a.get());` is not copying a pointer but is copying the value of `*(a.get())` into `c`. – NathanOliver Mar 13 '23 at 18:11
  • "we still have access" Experimental verification of laws is a risky business. If you successfully rob a bank, it does not prove that robbing banks is allowed, nor that you won't get shot next time you try. – n. m. could be an AI Mar 13 '23 at 19:01

1 Answers1

1

In general, storing the result of get member of smart pointers is not recommend. As in your snippet, the risk of UB is drastically increased in various ways. The only valid use case for this member functions is providing access for legacy API that expects none-owning pointers as parameters:

std::unique_ptr<
     std::FILE,
     decltype([](std::FILE * fp) {std::fclose(fp);})>
file=std::fopen("/mnt/x/test.txt", "rb+");
std::array<int,10> buf;
int count = 
    std::fread(buf.data(), sizeof(int), size(buf), file.get());
for(auto x:buf){
    if (!(count--))
       break;
    std::cout << x << "\n";
};

In the above fread is legacy API to read from a stream and needs a FILE* as its last parameter. Similar argument is true for data member of containers as well as c_str member of string-like types.

Red.Wave
  • 2,790
  • 11
  • 17
  • 1
    Also in modern C++ you have many situations in which you don't need an owning pointer. Like having functions that only required an object for the duration in which it is called (and you don't want to have a copy of the object), there you normally won't pass an owning pointer to it but a reference to the object, or a none owning pointer. Or if you have a parent child hierarchy in which you want to use `unique_ptr` and you still need a pointer to the parent. Or complex graph structures. – t.niese Mar 13 '23 at 18:44
  • For parameter passing ***one should typically use references***. In complex structures if all other options are off, `std::weak_ptr` is an important companion for `std::shared_ptr`. It is possible now to write complex code without the need to explicitly `delete` anything; Just crave to not use raw pointers and you'll find elegant coding much easier than errorprone programming. – Red.Wave Mar 13 '23 at 19:21
  • `It is possible now to write complex code without the need to explicitly delete anything` yes I absolutely agree with that. In modern code base, a raw point should be always not owning. `one should typically use references` ok but if a function takes an object as a reference, and you want to pass an object managed by a smart pointer, you need `get()`. About `std::weak_ptr` sure if you have shared ownership, but if you want unique ownership you can't use `std::weak_ptr` that's why I talked about `unique_ptr`. – t.niese Mar 13 '23 at 21:15
  • @t.niese you forgot the dereference operator(unary `*`). If you need a reference, `*ptr` is one of the defining properties of smart pointers. The reason behind calling the *smart **pointer*** is `sp::operator*` and `sp::operator->`. – Red.Wave Mar 14 '23 at 17:26