-
Notifications
You must be signed in to change notification settings - Fork 90
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
Conversation
There was a problem hiding this 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())) {} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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(), |
There was a problem hiding this comment.
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); } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More options:
-
Have
operator*
returnT
(by value copy). This wouldn't work foroperator->
though; it's the same issue as usingObject<>
asunique_ptr
'spointer
type: you wouldn't be able to dop->Get(...)
. -
Recapitulate the type punning we're using for the low level wrappers. Instead of wrapping
jobject*
inObject<>
, castjobject*
toObject<>*
. Then we can continue to useunique_ptr
with custom deleters. SinceObject<>
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 fromObject<BaseTag>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Delete
Object<>::operator=
???
There was a problem hiding this comment.
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 isconst
but returns non-constT&
.
That's not a apt comparison: the const
ness 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.
There was a problem hiding this comment.
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.
include/jni/unique_pointerlike.hpp
Outdated
return current; | ||
} | ||
|
||
explicit operator bool() const { return pointerlike; } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
Related: #32. |
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 ispointer
, which forUniqueLocalObject
(and friends) isPointerToValue
. This means thatget
copiesPointerToValue
-- in the case of*get()
, that's a copy into a temporary.PointerToValue
copies the held value.*get()
), producing a reference to the copy of the held value.operator*
returns. ThePointerToValue
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
andunique_ptr
, so this replacesunique_ptr
with a special RAII type that has the exact semantics we need.(Another thing that doesn't work is using
Object<>
as thepointer
type. That doesn't work becausestd::unique_ptr::operator->
returnspointer
, and we want it to returnObject<>*
, so thatp->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<>
), notPointerToValue<>
. You can see the typical change required in the hello.cpp example code.