Why is there no safe alternative to unique_ptr::operator*()?
Asked Answered
A

6

17

std::vector has the member function at() as a safe alternative to operator[], so that bound checking is applied and no dangling references are created:

void foo(std::vector<int> const&x)
{
  const auto&a=x[0];     // What if x.empty()? Undefined behavior!
  const auto&a=x.at(0);  // Throws exception if x.empty().
}

However, std::unique_ptr lacks the corresponding functionality:

void foo(std::unique_ptr<int> const&x)
{
  const auto&a=*x;       // What if bool(x)==false? Undefined behavior!
}

It would be great, if std::unique_ptr had such a safe alternative, say member ref() (and cref()) which never returns a dangling reference, but rather throws an exception. Possible implementation:

template<typename T>
typename add_lvalue_reference<T>::type
unique_ptr<T>::ref() const noexcept(false)
{
  if(bool(*this)==false)
    throw run_time_error("trying to de-refrence null unique_ptr");
  return this->operator*();
}

Is there any good reason why the standard doesn't provide this sort of thing?

Anthea answered 18/8, 2015 at 8:22 Comment(17)
It's easy to test for out-of-bounds (the bounds being part of the object on which the method/operator is invoked). How would it work for unique_ptr?Megdal
@Megdal OP wants to check for emptiness of a pointer, not boundsBipropellant
The title is somewhat confusing, my first idea was that the question is about bounds checking for something lik std::unique_ptr<int[]> pFoo(new int[n]); which can't be done.Plumate
"creates dangling reference if x.empty()". Actually, the behavior is explicitly undefined, which is the same as for *(unique_ptr<int>(NULL)), albeit the latter will most likely segfault.Truckload
@Truckload you're right. I added that fact in the comment of the code example.Anthea
I'll post this as a comment since it doesn't address your question fully: a possible workaround ideone.com/f4r8GsCupulate
Still, I'd actually like to know an answer, too. More generally, why the "managed" C++ pointer types don't come with an std::undefined_pointer_exception or similar. Since you can check for the validity of the pointers manually, the omission of the exception actually seems odd.Truckload
I suspect that it's because nullptr is the only invalid pointer you can (portably) test for, so the function isn't very safe, or even useful, at all.Adrenaline
@Truckload we've got assertions for that.Fragrance
@Fragrance That's not the same. Of course I can assert(x) to check the pointer, but I cannot catch the assertion. std::unique_ptr has a well defined invalid state that is not exposed to try/catch error handling. Of course it's not required, but it also another sinkhole down to undefined behavior when it would actually be possible to have defined behavior (even if you explicitly need to request it). So unique_ptr::get() (opposite to operator*()) could actually throw for invalid pointers. It doesn't.Truckload
@Truckload In fact, I agree that the reasoning behind vector.at() would be the same as the one behind an unique_ptr.at(), but what I wonder is why vector.at() exists. When you dereference a null pointer or access outside of an array... Then what ? You were going to use an object, but there's no object there. That's not an exceptional condition, but an invariant failure. Plus, if you want a meaningful exception, you need to intercept the null dereference, and throw your own exception from context. And if(!p) throw ...; looks better than try {...} catch(...) { throw ...; }.Fragrance
I have to agree with Quentin here. You want this functionality so you can avoid an if() statement and instead have a try...catch? Honestly, I didn't know that vector.at() was a function for a long time - I just checked the bounds of the vector before accessing it. But then again, I tend to use the "check preconditions" model rather than having exception handling everywhere (not saying either way is better).Sultana
As the inventor of unique_ptr I can say that there are lots of good answers here, and several good suggestions on how to get the behavior you want. And of those, none of them are demonstrably better than any of the rest. If one answer were to be standardized, someone would ask: "How come it wasn't done the other way?" And before you know it, the unique_ptr API would look a lot like the string API: bloated. The current API for unique_ptr is sufficient for you to do whatever you want to. No further help from the standard is needed.Calabro
@HowardHinnant The argument No further help from the standard is needed could (should) have been applied to obtain a leaner standard library API. So, it certainly is/was not a driving motivation in the design of the standard library. My question boils down to Why is the standard library API inconsistent regarding this (why has vector an at() but unique_ptr no ref())?Anthea
@Walter: Jonathan and MikeMB correctly addressed that point (and perhaps others, I am skimming). The standard library is a melting pot of volunteers. There is no entity overseeing the project with the power to hire and fire.Calabro
@HowardHinnant That sounds like a poorly run project. I would have hoped that the design and implementation of the standard library is guided by some overarching principles, that would avoid such inconsistencies.Anthea
@Walter: You're welcome to volunteer your services to improve the project. Stop by any time. I'm sure we could benefit from your wisdom.Calabro
B
13

I suspect the real answer is simple, and the same one for lots of "Why isn't C++ like this?" questions:

No-one proposed it.

std::vector and std::unique_ptr are not designed by the same people, at the same time, and are not used in the same way, so don't necessarily follow the same design principles.

Byblow answered 18/8, 2015 at 9:3 Comment(2)
I agree, or more because: it's how the standard is written. There was some discussion about it, relevant answer: #15204041Ornamental
@Ornamental unique_ptr::operator*() only throws if/because the pointer throws, not by its own.Anthea
C
17

unique_ptr was specifically designed as a lightweight pointer class with null-state detection (e.g. stated in optional in A proposal to add a utility class to represent optional objects (Revision 3))

That said, the capability you're asking is already in-place since operator* documentation states:

// may throw, e.g. if pointer defines a throwing operator*
typename std::add_lvalue_reference<T>::type operator*() const;

The pointer type is defined as

std::remove_reference<Deleter>::type::pointer if that type exists, otherwise T*

Therefore through your custom deleter you're able to perform any on-the-fly operation including null pointer checking and exception throwing

#include <iostream>
#include <memory>

struct Foo { // object to manage
    Foo() { std::cout << "Foo ctor\n"; }
    Foo(const Foo&) { std::cout << "Foo copy ctor\n"; }
    Foo(Foo&&) { std::cout << "Foo move ctor\n"; }
    ~Foo() { std::cout << "~Foo dtor\n"; }
};

struct Exception {};

struct InternalPtr {
    Foo *ptr = nullptr;
    InternalPtr(Foo *p) : ptr(p) {}
    InternalPtr() = default;

    Foo& operator*() const {
        std::cout << "Checking for a null pointer.." << std::endl;
        if(ptr == nullptr)
            throw Exception();
        return *ptr;
    }

    bool operator != (Foo *p) {
        if(p != ptr)
            return false;
        else
            return true;
    }
    void cleanup() {
      if(ptr != nullptr)
        delete ptr;
    }
};

struct D { // deleter
    using pointer = InternalPtr;
    D() {};
    D(const D&) { std::cout << "D copy ctor\n"; }
    D(D&) { std::cout << "D non-const copy ctor\n";}
    D(D&&) { std::cout << "D move ctor \n"; }
    void operator()(InternalPtr& p) const {
        std::cout << "D is deleting a Foo\n";
        p.cleanup();
    };
};

int main()
{
    std::unique_ptr<Foo, D> up(nullptr, D()); // deleter is moved

    try {
      auto& e = *up;      
    } catch(Exception&) {
        std::cout << "null pointer exception detected" << std::endl;
    }

}

Live Example

For completeness' sake I'll post two additional alternatives/workarounds:

  1. Pointer checking for a unique_ptr via operator bool

    #include <iostream>
    #include <memory>
    
    int main()
    {
        std::unique_ptr<int> ptr(new int(42));
    
        if (ptr) std::cout << "before reset, ptr is: " << *ptr << '\n';
        ptr.reset();
        if (ptr) std::cout << "after reset, ptr is: " << *ptr << '\n';
    }
    

    (This would probably be the clanest way to deal with the issue)

  2. An alternative solution, although messier, is to use a wrapper type which takes care of the exception handling

Cupulate answered 18/8, 2015 at 8:40 Comment(7)
That's exactly not the type of thing I asked for. I want to be able to choose between operator* and a safe alternative, exactly as with vector::operator[] and vector::at(). The latter only used in places where the extra costs are tolerable.Anthea
If you want it, submit a proposal to the committee and good luck. I see no other way.Cupulate
@Anthea You can get access to the underlying pointer with get() (part of the std::unique_ptr interface) and choose which form of access you want there. Since the pointer hook of std::unique_ptr is exactly designed for this sort of business, this answer is absolutely appropriate.Hachmin
@LucDanton: get also returns pointer, so would also be subject for the test.Adolphadolphe
@MatthieuM. That’s not how I’m reading the spec. Which test? It can’t be the one in InternalPtr since we’re returning that.Hachmin
@LucDanton: Ah, sorry; I thought you mean that get() would return a T* directly. Indeed gets returns an InternalPtr, so then you can unchecked accesses.Adolphadolphe
I should have said pointer instead of the ambiguous 'underlying pointer'.Hachmin
B
13

I suspect the real answer is simple, and the same one for lots of "Why isn't C++ like this?" questions:

No-one proposed it.

std::vector and std::unique_ptr are not designed by the same people, at the same time, and are not used in the same way, so don't necessarily follow the same design principles.

Byblow answered 18/8, 2015 at 9:3 Comment(2)
I agree, or more because: it's how the standard is written. There was some discussion about it, relevant answer: #15204041Ornamental
@Ornamental unique_ptr::operator*() only throws if/because the pointer throws, not by its own.Anthea
D
4

I can't say, why the committee decided not to add a safe dereferenciation method - the answer is probably "because it wasn't proposed" or "because a raw pointer hasn't one either". But it is trivial to write a free function template on your own that takes any pointer as an argument, compares it against nullptr and then either throws an excepion or returns a reference to the pointed to object.

If you don't delete it via a pointer to base class, it should be even possible to derive publicly from a unique_ptr and just add such a member function.

Keep in mind however that using such a checked method everywhere might incur a significant performance hit (same as at). Usualy you want to validate your parameters at most once, for which a single if statement at the beginning is much better suited.

There is also the school that says you should not throw exceptions in response to programming errors. Maybe the peopke in charge of designing unique_ptr belonged to this school, while the people designing vector(which is much much older) didn't.

Densify answered 18/8, 2015 at 8:54 Comment(3)
I don't think inheriting from unique_ptr is a wise idea (it may even be illegal), but I like the free function template. Ideally it has a static_assert to avoid it being called with anything but an ordinary pointer or a unique_ptr. Could you provide an implementation in your answer?Anthea
@Walter: But I agree, that it wouldn't be wise to derive from unique_ptr in any case.Densify
@Walter, it's not illegal.Byblow
D
3

One of the main goals of a smart pointer API design is to be a drop-in replacement with added value, no gotchas or side effects, and close to zero overhead. if (ptr) ptr->... is how safe access to bare pointer is usually done, the same syntax works nicely with smart pointers thus requiring no code change when one is replaced with the other.

An additional check for validity (say, to throw an exception) put inside a pointer would interfere with branch predictor and thus may have a knock-on effect on the performance, which may not be considered a zero cost drop-in replacement anymore.

Delict answered 18/8, 2015 at 8:45 Comment(5)
okay, that's an argument, but the branching applies equally well to vector::at(). One would only use unique_ptr::ref() if one knows that one deals with a unique_ptr, not when dealing with an opaque pointer-like object -- in this latter case one could indeed simply test bool(obj).Anthea
How would a completely separate method have any effect on unique_ptr being a drop in replacement for a raw pointer? Having extra methods on a class, especially a template class, doesn't generally add any overhead at all.Buckthorn
@BenjaminLindley - I'd explain it by "separation of concerns". It's a pointer - a lowest level language building block. Making it feature rich will cause more harm than good. Plus wrapping your favorite "smart_ptr" into things like "safe_ptr" "lazy_ptr" etc. is trivial, but extending the std::vector is probably not.Delict
a unique_ptr cannot be a drop-in replacement for a pointer. You can declare a class member of type pointer to object that has been forward-declared (but not defined). You cannot do that easily with a unique_ptr, its destructor must call the destructor of the pointed-to object (you can avoid this issue with a custom deleter, but that is not what I call drop-in replacement).Anthea
@Anthea - when implementing pimpl on std::unique_ptr you just declare enclosing class constructor in the header and define it in the compilation unit, possibly with = default;, not with custom deleter. Otherwise the trick with deleter shown in the MarcoA's answer settles the argument - the feature is already there, delegated to the deleter.Delict
O
2

You do have

operator bool()

Example from: cplusplusreference

// example of unique_ptr::operator bool
#include <iostream>
#include <memory>


int main () {
  std::unique_ptr<int> foo;
  std::unique_ptr<int> bar (new int(12));

  if (foo) std::cout << "foo points to " << *foo << '\n';
  else std::cout << "foo is empty\n";

  if (bar) std::cout << "bar points to " << *bar << '\n';
  else std::cout << "bar is empty\n";

  return 0;
}

unique_ptr is a simple wrapper to a raw pointer, no need to throw an exception when you can just check a boolean condition easily.

Edit: Apparently operator* can throw.

Exceptions 1) may throw, e.g. if pointer defines a throwing operator*

Maybe someone could shed some lights on hot to define a throwing operator*

Ornamental answered 18/8, 2015 at 8:28 Comment(5)
I knew all that, but that doesn't answer the question.Anthea
question is: why would you another method that adds the extra cost of handling exceptions when you can simply check the validity of the pointer with the more simple boolean operator? I can't find discussions on the committee when this was introduced, but I imagine this was the rationale behind.Ornamental
The n < v.size() check for std::vector isn't much more cumbersome, yet we have std::vector::at().Rubber
another point: imagine you could not declare any functions where you would use this as noexcept. I you think it makes really sense, I suggest you to submit a proposal to the standard commitee: isocpp.org/std/submit-a-proposal I, myself have never found the need for such a method, but maybe they will see it differently.Ornamental
@Ornamental take a look at this for the operator* exception throwingCupulate
A
2

Following from the suggestion of MikeMB, here is a possible implementation of a free function for dereferencing pointers and unique_ptrs alike.

template<typename T>
inline T& dereference(T* ptr) noexcept(false)
{
  if(!ptr) throw std::runtime_error("attempt to dereference a nullptr");
  return *ptr;
}

template<typename T>
inline T& dereference(std::unique_ptr<T> const& ptr) noexcept(false)
{
  if(!ptr) throw std::runtime_error("attempt to dereference an empty unique_ptr)");
  return *ptr;
}
Anthea answered 18/8, 2015 at 10:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.