-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Introduce "parented_ptr" wrapper class #1142
Conversation
If everyone is happy with the change, then I'll start going through the code base replacing naked new statements with |
767fcf3
to
3693277
Compare
BTW I've already done a bit of refactoring in #941 to wrap the core Mixxx services with shared pointers. I plan to merge it as soon as the 2.1 branch is cut (we decided it did not make sense to potentially destabilize 2.1). In general, we tend to avoid mass refactorings and instead clean up code as we touch it -- so I'd prefer it if you not go through and do some mass updates to get rid of new/delete. This results in fewer mistakes because it's easier to review piecemeal. |
You'll use
...and when might that be? :-p Or to put it more constructively, what work remains before you can do that? Why not just cut the branch now, and cherry-pick in the commits from the PRs that are currently under review once they are merged? I.e. make an exception to the "feature freeze" rule for a subset of the currently open PRs.
OK, I agree that it makes sense to make the changes gradually... How about if I localize the changes to the library related code? In order to make progress with #1117 I feel it's necessary to go through and review the ownership/lifetime of all the pointers, and it makes the most sense for me to clarify those issues by going through and enforcing the rules above. |
Also, do you agree with the guidelines themselves, and does 11f131e look OK? |
Good idea -- this will help us document object-tree ownership in the code, which will help readability.
I think the main benefit here is to have the code document the ownership of the objects, so I think we'll get the same benefit with something lighter-weight. What do you think about using a unique_ptr with a do-nothing deleter. Something like: template <typename T>
struct DoNothingDeleter {
void operator()(T*) {}
};
template <typename T>
class parented_ptr : std::unique_ptr<T, DoNothingDeleter<T>> {
public:
parented_ptr(T* t) : std::unique_ptr<T, DoNothingDeleter<T>>(t) {
DEBUG_ASSERT(t->parent() != nullptr);
}
~parented_ptr() = default;
}; or maybe instead of hijacking unique_ptr, a simple wrapper around the pointer with an We could call your |
It's been Real Soon Now for a while :) blocked on our build server. As of this week, working builds are back. I'd like to branch ~immediately. |
Awesome idea! |
Yep -- they sound good to me. One thing:
Did you mean p.release()? |
@rryan |
This rule does not work well, because p.get() does not moves the ownership. I think we should not use a unique_pointer where moving ownership by copy is required, because of an external API. I think we are talking basically about these cases and this case is and this:
for the first case we can do without overhead:
for the late case we need something like:
and
|
Of course we can, but I don't think we should, as it breaks the 5th rule. Instead we should encapsulate the raw pointer in the new
I edited the post sometime before you posted that after seeing @rryan's comment (that it should be |
I think @timrae meant to write p.release() there. I think it's fine to say that until the object has a parent, it should be in a unique_ptr in case the scope is unexpectedly terminated (e.g. someone sticks a 'return' statement somewhere between the point the object is created and receives a parent). the unique_ptr will prevent a leak in that case.
It's not safe to dereference p at any point after the constructor, since you don't know whether p has been deleted by its parent. |
0b06a91
to
4586ef7
Compare
UserSettingsPointer pConfig) | ||
: LibraryFeature(pConfig), | ||
: LibraryFeature(pConfig, pLibrary), |
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.
... and we already find our first bug ;)
c1ba9dd
to
e885005
Compare
assert(t->parent() != nullptr); | ||
} | ||
|
||
parented_ptr() : m_pPtr(nullptr) {} |
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.
Can't this be removed?
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, clear now, ditch my comment.
} | ||
|
||
operator bool() const { | ||
return m_pPtr != nullptr; |
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.
Can this ever be false?
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, it can. This should be named isNull()
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.
The snake case class name suggests some affinity to the standard C++ types which use operator bool() for this purpose.
*/ | ||
template <typename U> | ||
parented_ptr(const parented_ptr<U>& p, typename std::enable_if<std::is_convertible<U*, T*>::value, void>::type * = 0) | ||
: m_pPtr(p.get()) {} |
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 should be placed above since it is a constructor.
The assert parent not null is missing
|
||
bool operator!= (const parented_ptr& other) const { | ||
return m_pPtr != other.m_pPtr; | ||
} |
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 operators with the raw pointer can also be handy.
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.
Done
~parented_ptr() = default; | ||
|
||
T* get() const { | ||
return m_pPtr; |
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.
In case of shared and uique pointer get() indicates a pointer usage without passing ownership.
If we use get() here as well, it looks similar, but the whole new pointer does not have the ownership, so I think we can remove get and add a cast operator instead. This way we can get rid of the get cat() calls at the user side.
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.
The implicit cast operator hides unintended conversions to the raw pointer type.
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.
If we use get() here as well, it looks similar, but the whole new pointer does not have the ownership
It kind of does have ownership (by proxy) to the object tree. We could even add a release()
function that removes the parent and returns the raw pointer.
Also, it's highly preferable to keep the interface consistent with the other std pointer wrappers to simplify switching between them. I imagine it may be common to switch from shared to parented, and it will be unnecessarily annoying if you have to go and change all the .get() calls to something else.
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.
OK, good point.
What happens now with this case: Did you consider my psoido code? It may look like the pointer can be called future_parented_ptr() or might_parented_ptr() |
I force pushed a new commit with the changes requested. I also edited my previous comment (just woke up, sorry). @daschuer
Again, here I think |
OK, we can use unique_ptr::release() here. It just has the problem, that it has not the safety against failing to become a parent. A own unique_ptr pointer type that has a custom deleter would document at the make position that this pointer is intended to be owned by the object tree. |
We could do something like this to handle the variety of different ways an object can gain a parent: std::parent_checking_unique_ptr foo = make_parent_checking_unique<QMenu>();
// ...
foo.check_parented([this] (QMenu* pMenu) {
addMenu(pMenu);
});
// ^ check parented invokes the lambda with the result of release() then debug-asserts the parent is populated. Or it could be standalone method that you std::move a unique_ptr into: template <typename T>
void check_parented(std::unique_ptr<T> p, lambda) {
T* rp = p.release();
lambda(rp);
DEBUG_ASSERT(rp->parent() != nullptr);
} It's probably safe to assume that the object is not deleted by its parent immediately after the end of the lambda. While if you had a custom deleter, you have no way of knowing that by the end of the scope the custom unique_ptr lives in that the object was not already deleted by its parent -- so it's never safe to e.g. check whether something is parented in the deleter. IMO this is well into "perfect is the enemy of the good" territory here. I like parented_ptr / make_parented as they are now, and the guidelines sound good to me. We don't need to be automatons -- we can always make an exception if the situation warrants. |
OK, then it seams the best to me to switch to this solution if the future parent is already known
|
This is worse than using unique_ptr though, since it leaks the QMenu if we return early before hitting addMenu for some reason. |
No. You have missed that pFileMenu already is a parent form "this". We have an assertion for that. |
Oh sorry -- I misread. I don't know if that's always desirable -- maybe it's fine for menus, but for widgets, the moment you give it a parent the widget becomes visible if its parent is visible. This can expose an unstyled / incomplete widget and result in wasted draw events as we add more children / set the styling, etc. In this case, using a unique_ptr while you're setting things up seems reasonable to me. |
I don't think I understand the problem... auto pFileMenu = std::make_unique<QMenu>();
// ...
addMenu(pFileMenu.release()); Under what circumstances would auto pFileMenu = new QMenu();
// ...
addMenu(pFileMenu); |
If the parent object lives in a different thread. A new QObject lives in the creating thread. If you try to parent it to an object from a different thread, it is not adopted as a child and a warning message is issued. In the In the So if we use the first, it provides additional safety, but the later is no regression compared to the current situation. So all in all the situation is improved and a nice addition to Mixxx. @rryan |
} | ||
|
||
private: | ||
T* m_pPtr; |
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.
Can this be m_pObject?
Hm, I thought we constructed the skin with no connection to the MixxxMainWindow until we called setCentralWidget |
@timrae: |
Done (i.e. I updated this PR via force push). |
LGTM! Thank you. @rryan merge? |
SGTM |
One issue comes into my mind, where we have to be careful: This means we must not use parented_ptr as a member variable in objects which can outlive the parented object. In this cases we need to use a QPointer. Something like that should be part of the rules. |
You're right, I think that |
@daschuer I can't think of any way to get the compiler to enforce this, but we can at least get rid of the What do you think? I can't think of a good name for the new class.... Maybe |
We have a related discussion in daschuer#15 It looks like our parented_ptr is a bit itching compared to the concepts around it. We have the iso gpp guidlines:
If we have some day all our code refactored to "A raw pointer (a T*) is non-owning", our "parented_ptr" is somehow obsolete, since it dosn't matter for the user code how the lifetime of the pointer is guarded. The owning issue is a fight gains memory leaks, which are not the worst in the context of the library redesign which is instantiated only once or twice. Far more important in our case are possible crashers to invalid or null pointers. IMHO this leads to the following rule:
|
Allowing raw QPointer undermines the whole system in my opinion. If the object has no parent then use unique_ptr. If it is 100% obvious that there are no lifetime issues then use parented_ptr. Otherwise use |
we have to look at this case
In Qt 5 m_pObject1 and m_pObject2 are the same. addObject1 and addObject2 allow to take any non-owning pointer:
while addObject3 allows only parented pointers:
So it does make sense to store non parented_ptr in an object. And yes it undermines the some of our original ideas. But it should not be a problem to tweak our rules a bit for it. |
Can you please give an actual example from the code base?
…On 26 Jan. 2017 17:35, "Daniel Schürmann" ***@***.***> wrote:
we have to look at this case
class foo {
public:
addObject1(QObject* p1) {
m_pObject1 = p1;
}
addObject1(QObject* p2) {
m_pObject1 = p1;
}
addObject3(parented_ptr<QObject> p3) {
m_pObject3 = p3;
}
private:
QWeakPointer<QObject> m_pObject1
QPointer<QObject> m_pObject2
parented_ptr<QPointer<QObject> > m_pObject3
}
In Qt 5 m_pObject1 and m_pObject2 are the same.
addObject1 and addObject2 allow to take any non-owning pointer:
str::shared_pointer<QObject> p1;
str::weak_pinter<QObject> p2;
str::unique_ptr<QObject> p3;
QPointer<QObject> p4;
QObject* p5;
parented_ptr<QObject> p6;
foo.addObject1(p1.get());
foo.addObject1(p2.get());
foo.addObject1(p3.get());
foo.addObject1(p4.data());
foo.addObject1(p5));
foo.addObject1(p6));
while addObject3 allows only parented pointers:
QPointer<QObject> p4;
QObject* p5;
parented_ptr<QObject> p6;
foo.addObject3(make_parented(p4));
foo.addObject3(make_parented(QPointer(p5))));
foo.addObject3(p6));
So it does make sense to store non parented_ptr in an object.
It is always the case when the object has now requirement how an external
ownership is guaranteed.
And yes it undermines the some of our original ideas. But it should not be
a problem to tweak our rules a bit for it.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#1142 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/ACsA4lTz2O3deMuugS3ukeP3sd7AOBJ5ks5rWFrqgaJpZM4LqO0Q>
.
|
I do not know a place in our code where a function is used with different pointer types. If I have a call On the other hand If I have a call I like to be able to use the later version as well in Mixxx. |
This is an anti-pattern and would not compile in my proposed modifications due to deleted copy constructor... If a function is not to take ownership, the "correct" way is to
I guess you meant Anyway, I don't think it's useful to discuss "toy problems" like this without real world concrete examples... I'll submit a PR for my proposed modifications. |
Let's continue to discuss in #1161 |
Proposed change to the coding guidelines:
No naked new statements.
make_parented<T>
std::make_unique<T>
to create the object, orstd::make_shared<T>
if shared ownership is really necessary.p
withstd::make_unique<T>
, then later create a new variable usingparented_ptr(p.release())
once the underlying object gets a parent.std::move()
with aunique_ptr
if ownership is to be transferred, or if shared ownership is really necessary then useshared_ptr
.parented_ptr<T>
,unique_ptr<T>
, orshared_ptr<T>
.Reasons for the change
It's currently extremely difficult to review new and existing mixxx code for memory leaks and dangling pointers because the code base is scattered with naked new calls, and ownership of pointers is almost never clear. Modern C++ discourages using
new
anddelete
, so we should localize usage of these to a small number of places where the syntax makes ownership clear.