Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redo high-level ownership strategy #30

Merged
merged 3 commits into from
May 3, 2018
Merged

Redo high-level ownership strategy #30

merged 3 commits into from
May 3, 2018

Conversation

jfirebaugh
Copy link
Contributor

@jfirebaugh jfirebaugh commented Apr 11, 2018

The previous use of PointerToValue was unsafe. Consider the following code:

jni::UniqueLocalObject<Foo> f_ = ...;
const Object<Foo>& f = *f_;

Now f is a dangling reference. Why?

  • std::unique_ptr::operator* is defined as *get().
  • get()'s return type is pointer, which for UniqueLocalObject (and friends) is PointerToValue. This means that get copies PointerToValue -- in the case of *get(), that's a copy into a temporary.
  • Copying PointerToValue copies the held value.
  • Then the copy is dereferenced (*get()), producing a reference to the copy of the held value.
  • operator* returns. The PointerToValue temporary copy is destroyed, and the returned reference is now dangling.

AFAICT, there's no way to safely do what we're trying to do with PointerToValue and unique_ptr, so this replaces unique_ptr with a special RAII type that has the exact semantics we need.

(Another thing that doesn't work is using Object<> as the pointer type. That doesn't work because std::unique_ptr::operator-> returns pointer, and we want it to return Object<>*, so that p->Get(...) and so on work.)

Note that this is a breaking change, because e.g. the return type of release() is now one of the high-level types (Object<>, Array<>, Class<>), not PointerToValue<>. You can see the typical change required in the hello.cpp example code.

Copy link
Member

@kkaefer kkaefer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While reviewing this, I also noticed that our *Deleter functions can be constructed with the default constructor, which means that JNIEnv* will be null. Can we disallow creating without JNIEnv*?

It'd also be cool to see a test that confirms that the new behavior points to the correct jni::Object.

template < class U >
UniquePointerlike(UniquePointerlike<U, D>&& other)
: pointerlike(other.release()),
deleter(std::move(other.get_deleter())) {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this overload?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It permits construction of UniquePointerlike<Base> from UniquePointerlike<Derived> when Derived converts to Base. See #25/#26.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah cool; can we capture that in an enable_if?


void reset(T&& t = T())
{
T current = pointerlike;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we intentionally copying the object here instead of moving it? Can we make sure that a move-only type can be used as well?


public:
UniquePointerlike()
: pointerlike(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes that the contained type is default-constructible. unique_resource is essentially the same as this except without this caveat.

explicit operator bool() const { return pointerlike; }

T* operator->() const { return const_cast<T*>(&pointerlike); }
T& operator*() const { return const_cast<T&>(pointerlike); }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😨 Why are we casting away const here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking that UniquePointerlike::operator* should have pointer-like semantics even though the "pointed to" value is held as member data. I.e. be able to dereference a const pointer and get a non-const reference. Compare std::unique_ptr<T>::operator*, which is const but returns non-const T&.

I'm not sure this is correct though. The problem is that Object<>, Array<>, and Class<> have assignment operators, which effectively allow you to change what a const UniquePointerlike points to. That's dangerous.

We could have operator* return a const T&, but then you wouldn't be able to use Array<>::Set and Array<>::SetRegion.

We could make Array<>::Set and Array<>::SetRegion const, but that doesn't seem right. Shouldn't collection types always propagate const?

We could make UniquePointerlike a const-propagating type, providing const T& operator*() const and T& operator*(). But it seems to me that this doesn't really fix the problem -- we want reset() or move assignment to be the only way of changing what even a non-const UniquePointerlike points to.

We could abandon UniquePointerlike entirely and instead define a family of types:

  • LocalObject<>
  • LocalString
  • LocalArray<>
  • LocalClass<>
  • GlobalObject<>
  • GlobalString
  • GlobalArray<>
  • GlobalClass<>
  • WeakObject<>
  • WeakString
  • WeakArray<>
  • WeakClass<>

Each of these would have the same member functions as the existing un-prefixed types, except that they would all be move only and their destructor would release in the appropriate way.

Thoughts?

Copy link
Contributor Author

@jfirebaugh jfirebaugh Apr 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More options:

  • Have operator* return T (by value copy). This wouldn't work for operator-> though; it's the same issue as using Object<> as unique_ptr's pointer type: you wouldn't be able to do p->Get(...).

  • Recapitulate the type punning we're using for the low level wrappers. Instead of wrapping jobject* in Object<>, cast jobject* to Object<>*. Then we can continue to use unique_ptr with custom deleters. Since Object<> et al have no data (other than the pointer) and no vtable, I think this is safe, though unorthodox.

    For derived-to-base conversion to continue to work in this approach, Object<DerivedTag> would need to C++-inherit from Object<BaseTag>.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Delete Object<>::operator= ???

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compare std::unique_ptr<T>::operator*, which is const but returns non-const T&.

That's not a apt comparison: the constness of that operator refers to the fact that the container can't be changed (i.e. the pointer it holds cannot be replaced). A container like std::unique_ptr<const T> would still return a const T& with that operator.

Thinking about this more, I think it makes sense to implement the approach #32 suggests in combination with creating a family of holder types.

Copy link
Contributor Author

@jfirebaugh jfirebaugh Apr 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My point was that unique_ptr does not propagate const-ness, and in general, pointer types don't propagate const-ness. You can mutate the pointed-to object through a const pointer; to make the pointed-to object itself const requires an additional const in a different place (unique_ptr<const T>, as you say, or const T const *).

I wound up fixing the hole by deleting operator=s and friending UniquePointerlike so that only it can update the pointer. I don't think reassignability was much used for Object/Class/Array, so this feels like a better solution than duplicating member functions into parallel class families, and then dealing with needing conversions between them. It's also more backwards compatible, as it doesn't require a lot of rewriting -> to . in client code.

return current;
}

explicit operator bool() const { return pointerlike; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This requires the contained type to be castable to boolean. Is that what we want?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes -- UniquePointerlike is intended to be used only with Object<>, Array<>, and Class<>, not as a general-purpose smart pointer.

@jfirebaugh
Copy link
Contributor Author

Related: #32.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants