5

I was reading Effective C++ 3rd Edition. In page 70, the author says:

Like virtually all smart pointer classes, tr1::shared_ptr and auto_ptr also overload the pointer dereferencing operators (operator-> and operator*), and this allows implicit conversion to the underlying raw pointers (...)

He then shows an example with shared_ptr (which was part of tr1 at the time) featuring implicit conversion based on a class named Investment:

shared_ptr<Investment> pi1();
bool taxable1 = !(pi1->isTaxFree());
                    ^implicit conversion

shared_ptr<Investment> pi2();
bool taxable2 = !((*pi2).isTaxFree());
                    ^implicit conversion

Well, I have since then written a few test cases with unique_ptr and they hold up.

I also found out about unique_ptr supporting arrays and shared_ptr also going to (see note). However, in my testing, implicit conversion does not seem to work for smart pointers around arrays.

Example: I wanted this to be valid...

unique_ptr<int[]> test(new int[1]);

(*test)[0] = 5;

but it is not, according to my compiler (Visual C++ 2015 Update 3).

Then, from a little research, I found some evidence suggesting that implicit conversion isn't supported at all... like this one for instance: https://herbsutter.com/2012/06/21/reader-qa-why-dont-modern-smart-pointers-implicitly-convert-to.

At this point I am in doubt. Is it supported (by the Standard), or is it not?


Note: The book might be a bit outdated on this topic, since the author also says on page 65 that "there is nothing like auto_ptr or tr1::shared_ptr for dinamically allocated arrays, not even in TR1".

Marc.2377
  • 7,807
  • 7
  • 51
  • 95
  • `*test[0] = 5;` You deference the result of `unique_ptr::operator[]`. Of course it doesn't work for a plain `int`. – StoryTeller - Unslander Monica Nov 16 '16 at 06:26
  • @StoryTeller Fair enough. I had that fixed, I think... – Marc.2377 Nov 16 '16 at 06:32
  • `unique_ptr` has no `operator*()`. `unique_ptr` has`operator*()`, while `unique_ptr` has `operator[]()` instead. So set a value, simply use: `test[0] = 5;` – David Scarlett Nov 16 '16 at 06:45
  • If you are just interested in an array in `std::unique_ptr`, instead consider using `std::unique_ptr>` or `std::unique_ptr>` where `N` is the length of an array. – Ohashi Nov 16 '16 at 06:50
  • @Ohashi, better just use `std::vector` directly. It will happily move itself when needed. – StoryTeller - Unslander Monica Nov 16 '16 at 06:52
  • @Ohashi - what would be the benefit of `unique_ptr>` over just `vector`? The only benefit I can think of is that you can pass the former around more cheaply if the vector's value type doesn't permit move operations. It just seems very awkward. – David Scarlett Nov 16 '16 at 06:55
  • 4
    FWIW, Scott Meyers (Effective Modern C++) had this to say: "The existence of std::unique_ptr for arrays should be of only intellectual interest to you, because std::array, std::vector, and std::string are virtually always better data structure choices than raw arrays. About the only situation I can conceive of when a std::unique_ptr would make sense would be when you’re using a C-like API that returns a raw pointer to a heap array that you assume ownership of." – David Scarlett Nov 16 '16 at 06:55
  • @DavidScarlett, you can still move the vector itself even if the template parameter isn't moveable. – StoryTeller - Unslander Monica Nov 16 '16 at 06:56
  • 1
    @StoryTeller @DavidScarlett I think I confused it with `std::shared_ptr` because I was thinking about sharing the same instance of a `std::vector`. Thank you for pointing it out. – Ohashi Nov 16 '16 at 06:57
  • 1
    @StoryTeller Thats right, sorry, I was getting mixed up with the requirement that the value type's move constructor be declared `nothrow`, but that's for when the vector is moving elements on resize, not for when the vector is being moved. – David Scarlett Nov 16 '16 at 07:15

2 Answers2

5

Well, here's the thing. There is no implicit conversion to the underlying pointer, you have to call a specific get member function (it's a theme in the standard library, think std::string::c_str).

But that's a good thing! Implicitly converting the pointer can break the guarantees of unique_ptr. Consider the following:

std::unique_ptr<int> p1(new int);
std::unique_ptr<int> p2(p1);

In the above code, the compiler can try to pass p1s pointee to p2! (It won't since this call will be ambiguous anyway, but assume it wasn't). They will both call delete on it!

But we still want to use the smart pointer as if it was a raw one. Hence all the operators are overloaded.


Now let's consider your code:

(*test)[0] = 5;

It calls unique_ptr::operator* which produces an int&1. Then you try use the subscript operator on it. That's your error.
If you have a std::unique_ptr<int[]> than just use the operator[] overload that the handle provides:

test[0] = 5;

1 As David Scarlett pointed out, it shouldn't even compile. The array version isn't supposed to have this operator.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • Implicit conversion is nice to have though. Imagine implementing an Objective C-like runtime interface for C++. You would have something like (NSString::alloc())->autorelease(), casting between NSString, and NSObject, and NSSpecialString, and so on requires you to resort to despirate means that look similar to Microsoft IUnknown's QueryInterface. I am pretty sure with our current C++ we can just automate this process. – Dmytro Nov 16 '16 at 06:45
  • 1
    Actually, only the single-object version, `unique_ptr` has `operator*()`. The array version, `unique_ptr` has `operator[]()` instead. So rather than returning the first element, `(*test)` shouldn't even compile. GCC gives: `error: no match for 'operator*' (operand type is 'std::unique_ptr')` – David Scarlett Nov 16 '16 at 06:48
  • @Dmitry, I agree that it's nice to have, that's why Andrei Alexandrescu provides that option as a policy in "Modern C++ Design"; with `explicit` conversion operators, it's even less error prone most of the time. But the standard library sticks to the more cautious approach, which I can't really fault. – StoryTeller - Unslander Monica Nov 16 '16 at 06:48
  • @DavidScarlett, good catch. I completely forgot it shouldn't even work. – StoryTeller - Unslander Monica Nov 16 '16 at 06:50
  • Cool! I agree that implicit conversion is not very great, and calling `get()` is no big deal. Guess I was just confused by what the book says and the compiler does vs what can be found in the web. Thanks. – Marc.2377 Nov 16 '16 at 06:52
  • @DavidScarlett to be fair the array version of a type is a struct/container type. This is the desirable behavior. Arrays do not preserve their form when tossed around, they get coersed into pointers unless you wrap them in a struct. – Dmytro Nov 16 '16 at 06:53
  • Regarding note ¹ based on @DavidScarlett 's comment, it indeed does not compile. – Marc.2377 Nov 16 '16 at 06:55
1

As StoryTeller indicates, implicit conversions would ruin the show, but I'd like to suggest another way of thinking about this:

Smart pointers like unique_ptr and shared_ptr try to hide the underlying raw pointer because they try to maintain a certain kind of ownership semantics over it. If you were to freely obtain that pointer and pass it around, you could easily violate those semantics. They still provide a way to access it (get), since they couldn't stop you completely even if they wanted to (after all you can just follow the smart pointer and get the address of the pointee). But they still want to put a barrier to make sure you don't access it accidentally.

All is not lost though! You can still gain that syntactic convenience by defining a new kind of smart pointer with very weak semantics, such that it can safely be implicitly constructed from most other smart pointers. Consider:

// ipiwdostbtetci_ptr stands for : 
// I promise I wont delete or store this beyond the expression that created it ptr
template<class T>
struct ipiwdostbtetci_ptr {
    T * _ptr;
    T & operator*() {return *_ptr;}
    T * operator->(){return _ptr;}
    ipiwdostbtetci_ptr(T * raw): _ptr{raw} {}
    ipiwdostbtetci_ptr(const std::unique_ptr<T> & unq): _ptr{unq.get()} {}
    ipiwdostbtetci_ptr(const std::shared_ptr<T> & shr): _ptr{shr.get()} {}
};

So, what's the point of this satirically named smart pointer? It's just a kind of pointer that's verbally given a contract that the user will never keep it or a copy of it alive beyond the expression that created it and the user will also never attempt to delete it. With these constraints followed by the user (without the compiler checking it), it's completely safe to implicitly convert many smart pointers as well as any raw pointer.

Now you can implement functions that expect a ipiwdostbtetci_ptr (with the assumption that they'll honor the semantics), and conveniently call them:

void f(ipiwdostbtetci_ptr<MyClass>);
...
std::unique_ptr<MyClass> p = ...
f(p);
enobayram
  • 4,650
  • 23
  • 36