8

I was reading Top 10 dumb mistakes to avoid with C++11 smart pointer. Number #5 reads:

Mistake # 5 : Not assigning an object(raw pointer) to a shared_ptr as soon as it is created !

int main()
{
    Aircraft* myAircraft = new Aircraft("F-16"); 
    shared_ptr<aircraft> pAircraft(myAircraft);
    ...
    shared_ptr<aircraft> p2(myAircraft); 
    // will do a double delete and possibly crash
}

and the recommendation is something like:

Use make_shared or new and immediately construct the pointer with it.

Ok, no doubt about it the problem and the recommendation. However I have a question about the design of shared_ptr. This is a very easy mistake to make and the whole "safe" design of shared_ptr could be thrown away by very easy-to-detect missuses.

Now the question is, could this be easily been fixed with an alternative design of shared_ptr in which the only constructor from raw pointer would be that from a r-value reference?

template<class T>
struct shared_ptr{
    shared_ptr(T*&& t){...basically current implementation...}
    shared_ptr(T* t) = delete; // this is to...
    shared_ptr(T* const& t) = delete; // ... illustrate the point.
    shared_ptr(T*& t) = delete;
    ...
};

In this way shared_ptr could be only initialized from the result of new or some factory function.

Is this an underexploitation of the C++ language in the library? or What is the point of having a constructor from raw pointer (l-value) reference if this is going to be most likely a misuse?

Is this a historical accident? (e.g. shared_ptr was proposed before r-value references were introduced, etc) Backwards compatibility?

(Of course one could say std::shared_ptr<type>(std::move(ptr)); that that is easier to catch and also a work around if this is really necessary.)

Am I missing something?

alfC
  • 14,261
  • 4
  • 67
  • 118

3 Answers3

5

Pointers are very easy to copy. Even if you restrict to r-value reference you can sill easily make copies (like when you pass a pointer as a function parameter) which will invalidate the safety setup. Moreover you will run into problems in templates where you can easily have T* const or T*& as a type and you get type mismatches.

So you are proposing to create more restrictions without significant safety gains, which is likely why it was not in the standard to begin with.

The point of make_shared is to atomize the construction of a shared pointer. Say you have f(shared_ptr<int>(new int(5)), throw_some_exception()). The order of parameter invokation is not guaranteed by the standard. The compiler is allowed to create a new int, run throw_some_exception and then construct the shared_ptr which means that you could leak the int (if throw_some_exception actually throws an exception). make_shared just creates the object and the shared pointer inside itself, which doesn't allow the compiler to change the order, so it becomes safe.

Sorin
  • 11,863
  • 22
  • 26
  • Pointers are easy to copy, but not to a `shared_ptr` if `T*&&` is the only constructor. `T* const` or `T*&` both would be deleted so I don't understand your point about "type mismatches". (Also template deduction doesn't happens in the constructor.) I am not questioning the use of `make_shared`, on the contrary, this would encourage to use it. – alfC May 18 '16 at 08:17
  • Finally, I don't understand your point about the scenario with the thrown exception on function invocation, that seems to be a separate problem, perhaps that needs a separate solution, like making the constructor of `shared_ptr` private and force a friend function `make_shared` to be the only way to create a `shared_ptr`. – alfC May 18 '16 at 08:17
  • @alfC you can have `X* a = new X; X* b = a; shared_ptr(a); shared_ptr(b)` This is an example of pointer copy. Or you can have `X* a = new X; f(a); shared_ptr(a);` where `f(X* a_copy) { shared_ptr(a_copy); }`. Even if we restrict to T&& constructor only the code above would compile and fail as before. – Sorin May 18 '16 at 08:59
  • @alfC for type mismatches. If you delete those constructors you can run into cases where templates would be greatly simplified if you had them, or they are not written correctly (with all type deduction and stripping) so they would generate code that would need the deleted constructor and yield errors. That point is not about safety but about ease of use. – Sorin May 18 '16 at 09:01
  • As far as I know, **the standard have been updated for the order of evaluation**. While order is still implementation defined, a compiler must fully evaluate each argument now and thus a compiler cannot cal `new int`, throw and then call `shared_ptr` constructor anymore. Thus, I think that `void f(shared_ptr a, shared_ptr b) { }; f(new int(1), new int(2));`would not leak anymore in case of an exception. – Phil1970 Apr 12 '21 at 23:41
  • @Phil1970 That sounds very cool. Do you have a link to the updated standard section? – Sorin Apr 27 '21 at 08:21
  • Well, I found a few interesting links with Google. Those rules have been mainly updated for C++17: **(1)** https://en.cppreference.com/w/cpp/language/eval_order, **(2)** https://isocpp.org/blog/2016/08/quick-q-why-doesnt-cpp-have-a-specified-order-for-evaluating-function-argum, **(3)** https://riptutorial.com/cplusplus/example/19369/evaluation-order-of-function-arguments – Phil1970 Apr 27 '21 at 23:25
2

I do not have any special insight into the design of shared_ptr, but I think the most likely explanation is that the timelines involved made this impossible:

The shared_ptr was introduced at the same time as rvalue-references, in C++11. The shared_ptr already had a working reference implementation in boost, so it could be expected to be added to standard libraries relatively quickly.

If the constructor for shared_ptr had only supported construction from rvalue references, it would have been unusable until the compiler had also implemented support for rvalue references.

And at that time, compiler and standards development was much more asynchronous, so it could have taken years until all compiler had implemented support, if at all. (export templates were still fresh on peoples minds in 2011)

Additionally, I assume the standards committee would have felt uncomfortable standardizing an API that did not have a reference implementation, and could not even get one until after the standard was published.

Benno
  • 5,288
  • 5
  • 42
  • 60
1

There's a number of cases in which you may not be able to call make_shared(). For example, your code may not be responsible for allocating and constructing the class in question. The following paradigm (private constructors + factory functions) is used in some C++ code bases for a variety of reasons:

struct A {
  private:
     A();
};

A* A_factory();

In this case, if you wanted to stick the A* you get from A_factory() into a shared_ptr<>, you'd have to use the constructor which takes a raw pointer instead of make_shared().

Off the top of my head, some other examples:

  • You want to get aligned memory for your type using posix_memalign() and then store it in a shared_ptr<> with a custom deleter that calls free() (this use case will go away soon when we add aligned allocation to the language!).
  • You want to stick a pointer to a memory-mapped region created with mmap() into a shared_ptr<> with a custom deleter that calls munmap() (this use case will go away when we get a standardized facility for shmem, something I'm hoping to work on in the next few months).
  • You want to stick a pointer allocated by into a shared_ptr<> with a custom deleter.
blelbach
  • 436
  • 3
  • 8
  • I am in favor of factories. My original question is about the constructor of shared_ptr, so that only accepts input from factories, make_shared or otherwise. – alfC May 19 '16 at 16:44