4

I'm relatively new to cpp, and am learning about smart points. I'm wondering the following:

Why is constructing an std::unique_ptr with an lvalue allowed?

Wouldn't it be safer to only allow the construction of an std::unique_ptr with rvalues to avoid evil things?

std::unique_ptr<int> createInt() {
    int* a = new int(99);
    std::unique_ptr<int> foo(a);
    delete a;
    return foo;
}

I realize you'd have to be crazy to write something like that but I'd be nice to have the compiler yell at you for it. So I'm left wondering, why is lvalue initialization of unique_ptr a thing?

EDIT: User @aler egal put my thoughts more elagantly:

"In principle, you could have a constructor unique_ptr<int>(int*&& ptr) which assumes ownership of ptr and then sets it to null. That would prevent a use-after-free in this specific example (because you'd be forced to std::move(a) and because calling delete on a null pointer has no effect) but it would be a very strange anti-pattern."

kesarling He-Him
  • 1,944
  • 3
  • 14
  • 39
frankelot
  • 13,666
  • 16
  • 54
  • 89
  • 5
    An r-value of a pointer (and other primitives) isn't actually cleared/nulled by "moving" it, so it wouldn't help here. – ShadowRanger Sep 03 '20 at 03:13
  • 6
    C++ is not a nanny language. It gives you plenty of rope to shoot yourself in the foot. – Eljay Sep 03 '20 at 03:13
  • 2
    _In principle_, you could have a constructor `unique_ptr(int*&& ptr)` which assumes ownership of `ptr` and then sets it to `null`. That would prevent a use-after-free _in this specific example_ (because you'd be forced to `std::move(a)` and because calling `delete` on a null pointer has no effect) but it would be a very strange anti-pattern. A far better approach would be to avoid owning raw pointers altogether. – alter_igel Sep 03 '20 at 03:20

2 Answers2

6

This is a classic example of what a dangling pointer is! In contrast to what smart pointers may seem, they are simply wrappers over normal (raw) pointers, which can manage memory on their own and give you some added functionalities.

Consider the following example:

int* someFunc() {
    int* ptr;
    int* ptr2 = ptr;
    delete ptr;
    return ptr2;
}

This is what you are essentially doing.
They have been made such that they can be used instead of raw owning pointers at all times; meaning, they can be dangled too! So, if they are not allowed lvalue initialisation, that's one use case where you can't use a smart pointer, although I agree, this is one case where you are not to use either!

The above code with smart pointers will be exactly what you tried.

Now, C++ leaves logic to you... entirely. If you want to shoot yourself in the foot, go ahead, C++ won't bark. C++ does not check for memory usage and buffer overflows and access of deleted memory. Simply speaking, it's the job of a C++ programmer and if he/she wants to shoot himself/herself in the foot, he/she's free to do so!


Also, it is usually recommended to use std::make_ptr() over the ctor, as the former is exceptions enabled

kesarling He-Him
  • 1,944
  • 3
  • 14
  • 39
2

There is a well-known design principle that suggests designs be kept not just simple, but stupid-simple. Adding complexity to the simplest possible implementation requires a justification. In this case, the suggested justification is the avoidance of evil things like the following:

std::unique_ptr<int> createInt() {
    int* a = new int(99);
    int* b = a;
    std::unique_ptr<int> foo(b);
    delete a;
    return foo;
}

Oops. Looks like I changed something (introduced a new variable, b), but the same evil result is there. Only... in this form, forcing move-construction would not help. Even if b was set to null after construction, the value of a would be untouched. (This form would require something analogous to move-constructing a unique_ptr from a shared_ptr, but such a constructor does not exist – for good reason.)

So you end up introducing complications to the implementation of the constructor (it would need to null-out the provided pointer) and to uses of the constructor (foo(a) would need to become foo(std::move(a))), yet the problem is not really solved. If the language's highest goal was safety, the complication might still be justified. However, C++ ranks performance (you only pay for what you use) over safety.


A lot of the safety features of C++ are there to ensure something good gets done, such as releasing memory. There is very little done to stop something bad from being done, such as releasing the same memory twice. Some bad things trigger warnings from some compilers, but in the end, the programmer gets what the programmer wants asks for.

JaMiT
  • 14,422
  • 4
  • 15
  • 31
  • I don't get it. "Oops. Looks like I changed something, but the same evil result is there." No you didnt't, you are still creating the unique_ptr using an lvalue. – frankelot Sep 04 '20 at 21:06
  • @FRR I didn't change something? I don't recall seeing a variable named `b` in your version. If I look at just the last three lines of your version, I see a problem. If I look at just the last three lines of my version, the problem is not evident. – JaMiT Sep 04 '20 at 21:34
  • "If I look at just the last three lines of my version, the problem is not evident" Exactly, that's my complaint. This is why I'm asking this question. I don't understand what's new about your code. It illustrates the same thing as mine. If you were able to cause this problem using rvalue when creating the shared_ptr then I would be impressed – frankelot Sep 04 '20 at 22:51
  • @FRR **What's new:** The variable `b`. **In your version,** you pass `a` to the constructor then delete `a`. So if the constructor were to clear the variable used as a parameter, then the deletion would be given a null pointer. **In my version,** I pass *`b`* to the constructor then delete `a`. So if the constructor were to clear the variable used as a parameter, then *the deletion would be given the original pointer.* – JaMiT Sep 05 '20 at 00:54
  • I get that. But again, that's my point, in my ideal scenario you couldn't do this `std::unique_ptr foo(b);`... The only way to create a `smart_pointer` would be to use an rvalue `std::unique_ptr foo(new xxx);`. Your scenario is just another example of the problem I'm trying to illustrate in my original question. – frankelot Sep 05 '20 at 03:27
  • 2
    @FRR No, you are wrong. Forcing the use of rvalues does not force one to nest a `new` inside the parameter; `std::unique_ptr foo(std::move(b));` fits perfectly into your scenario. – JaMiT Sep 05 '20 at 03:30