16

So when using shared_ptr<Type> you can write:

shared_ptr<Type> var(new Type());

I wonder why they didn't allow a much simpler and better (imo):

shared_ptr<Type> var = new Type();

Instead to achieve such functionality you need to use .reset():

shared_ptr<Type> var;
var.reset(new Type());

I am used to OpenCV Ptr class that is a smart pointer that allows direct assignment and everything works fine

Russia Must Remove Putin
  • 374,368
  • 89
  • 403
  • 331
giò
  • 3,402
  • 7
  • 27
  • 54

5 Answers5

29

The syntax:

shared_ptr<Type> var = new Type();

Is copy initialization. This is the type of initialization used for function arguments.

If it were allowed, you could accidentally pass a plain pointer to a function taking a smart pointer. Moreover, if during maintenance, someone changed void foo(P*) to void foo(std::shared_ptr<P>) that would compile just as fine, resulting in undefined behaviour.

Since this operation is essentially taking an ownership of a plain pointer this operation has to be done explicitly. This is why the shared_ptr constructor that takes a plain pointer is made explicit - to avoid accidental implicit conversions.


The safer and more efficient alternative is:

auto var = std::make_shared<Type>();
Maxim Egorushkin
  • 131,725
  • 17
  • 180
  • 271
  • 4
    @giò It is a big problem indeed. – Maxim Egorushkin May 17 '16 at 12:10
  • 1
    @giò A shared pointer assumes it owns it's pointer (and shares it only with other shared pointers). If you passed a raw pointer to a shared pointer, chances are that another piece of code assumed it owned the raw pointer and would try to delete it at some point. But so would the shared pointer! So yeah, double delete is no good at all. – KABoissonneault May 17 '16 at 12:56
  • I wouldn't call std::make_shared<> **safer**... Used in conjunction with a std::weak_ptr, std::make_shared<> can have adverse effects : the memory occupied by the object persists until all weak owners are also destroyed. The "classical" way is less efficient because it does two allocations (object + control block), but the memory will be released as soon as all the shared_ptr<> are destroyed, regardless of the remaining weak_ptr<>. – Adrian B. Aug 10 '21 at 01:00
21

The issue with allowing a raw pointer to be implicitly converted into a std::shared_ptr can be demonstrated with

void foo(std::shared_ptr<int> bar) { /*do something, doesn't matter what*/ }

int main()
{
    int * bar = new int(10);
    foo(bar);
    std::cout << *bar;
}

Now if the implicit conversion worked the memory bar points to would be deleted by the shared_ptr destructor at the end of the foo(). When we go to access it in std::cout << *bar; we now have undefined behavior as we are dereferencing a deleted pointer.

In your case you create the pointer directly at the call site so it does not matter but as you can see from the example it can cause problems.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
16

Allowing this allows you to call functions with pointer arguments directly, which is error prone because you're not necessarily aware at call site that you're creating a shared pointer from it.

void f(std::shared_ptr<int> arg);
int a;
f(&a); // bug

Even if you disregard this, you create the invisible temporary at the call site, and creating shared_ptr is quite expensive.

milleniumbug
  • 15,379
  • 3
  • 47
  • 71
  • 1
    Unless you don't create it with `new` because then you are `delete`ing memory you haven't `new`ed. – milleniumbug May 17 '16 at 12:09
  • @giò, It can cause overload ambiguity, and making a `shared_ptr` is more expensive than most people would like if it were implicit. For other classes, it can be a non-obvious transformation, too. For example, passing `5` and having it turn into a `std::vector` would be very unintuitive. – chris May 17 '16 at 12:10
8

Why [doesn't] shared_ptr permit direct assignment [copy initialization]?

Because it is explicit, see here and here.

I wonder what the rationale [is] behind it? (From a comment now removed)

TL;DR, making any constructor (or cast) explicit is to prevent it from participating in implicit conversion sequences.

The requirement for the explicit is better illustrated with the shared_ptr<> is an argument for a function.

void func(std::shared_ptr<Type> arg)
{
  //...
}

And called as;

Type a;
func(&a);

This would compile, and as written and is undesired and wrong; it won't behave as expected.

It gets further complicated with adding user defined (implicit) conversions (casting operators) into the mix.

struct Type {
};

struct Type2 {
  operator Type*() const { return nullptr; }
};

Then the following function (if not explicit) would compile, but offers a horrible bug...

Type2 a;
func(a);
Niall
  • 30,036
  • 10
  • 99
  • 142
8

I wonder why they didn't allow a much simpler and better...

Your opinion will change as you become more experienced and encounter more badly written, buggy code.

shared_ptr<>, like all standard library objects is written in such as way as to make it as difficult as possible to cause undefined behaviour (i.e. hard to find bugs that waste everyone's time and destroy our will to live).

consider:

#include<memory>

struct Foo {};

void do_something(std::shared_ptr<Foo> pfoo)
{
  // ... some things
}

int main()
{
  auto p = std::make_shared<Foo>(/* args */);
  do_something(p.get());
  p.reset();  // BOOM!
}

This code cannot compile, and that's a good thing. Because if it did, the program would exhibit undefined behaviour.

This is because we'd be deleting the same Foo twice.

This program will compile, and is well-formed.

#include<memory>

struct Foo {};

void do_something(std::shared_ptr<Foo> pfoo)
{
  // ... some things
}

int main()
{
  auto p = std::make_shared<Foo>(/* args */);
  do_something(p);
  p.reset();  // OK
}
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142