-
-
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
Beats: Use std::make_shared/shared_from_this() #4336
Conversation
Do not return a nullptr to avoid unintended crashes!
Was about to PR the same thing. 😄 |
@tcoyvwac Do you know of a better solution than the artificial, protected type tag for making the public constructors inaccessible? The other changes should be non-controversial. |
The design is still unfinished. An immutable, sharable wrapper should wrap plain, mutable C++ classes that do not need to care about the sharing that happens in the outer context. I am not going to work on this clean separation of functional from infrastructure code. |
In the long term we need to think about applying the same principles to other areas like |
Did you have a look how Qt implements it implicit sharing and private implementation pattern? I think this would work here also fine and release the using code form all the pointer and stuff. The internal container holding the immutable beats can be implemented as a cpp file only class. The outside copy-able wrapper will point to the internal container and can be handled by value in the code. I have in mind to make just like QByteArray, In your case it is an array of beats and their metadata. In Qt 6 they call it DataPointer d. We don't even need to hide the the internal class, because we have no public API. What do you think? |
I am not interested in rewriting these classes besides the obvious improvements and safety fixes provided by this PR. Please review the PR or I will close it. |
If you have ideas that extend the scope of this PR then please provide a follow-up PR for review. |
No, currently, I can not think of a way. The intent though of your PR is very clear and also agree with you that refactoring classes (via an API analysis approach) right now is out of the scope of this PR. This PR is just about locking down the dynamic parts of the projects around smart pointers (using modern methods). The faster the "safety" PRs are merged, then the more flexibility there is (or "will be") in redesigning some current API structures (safely). |
@@ -98,7 +98,8 @@ BeatsPointer BeatGrid::makeBeatGrid( | |||
// Calculate beat length as sample offsets | |||
const audio::FrameDiff_t beatLengthFrames = 60.0 * sampleRate / bpm.value(); | |||
|
|||
return BeatsPointer(new BeatGrid(sampleRate, subVersion, grid, beatLengthFrames)); | |||
return std::make_shared<BeatGrid>( | |||
MakeSharedTag{}, sampleRate, subVersion, grid, beatLengthFrames); |
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 you use here the explicit version, where above you use only the braced tantalizer?
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.
Because std::make_shared() is not able to deduce the type and compilation fails. Try it.
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 think we should be explicit everywhere.
src/track/beatgrid.cpp
Outdated
BeatGrid::BeatGrid( | ||
MakeSharedTag, | ||
const BeatGrid& other) | ||
: BeatGrid({}, other, other.m_grid, other.m_beatLengthFrames) { |
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 you don't pass the MakeSharedTag, but create a new one?
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.
Because type tags are not supposed to carry any information and should be eliminated during compilation. Passing the instance is not desired in this special case. Just a hint for the compiler to ignore the unnamed parameter.
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.
Thank you for the PR, I have left some comments for better readability.
src/track/beats.h
Outdated
public: | ||
virtual ~Beats() = default; | ||
|
||
BeatsPointer clone() const { |
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 term clone() is misleading here, because the Beats object is not cloned in terms of making an identical copy.
You just copy a shared pointer form a hidden weak pointer. In Qt it is called toStrongRef()
In our case maybe toBeatsPointer() or getBeatsPointer() or such.
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.
Cloning an immutable object is trivial be sharing its reference.
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.
Conceptually this is still a clone operation.
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 confused when reading it the first time. You can either add documentation that this does not really clone, or change the name. I would prefer the later.
/// Type tag for making public constructors of derived classes inaccessible. | ||
/// | ||
/// The constructors must be public for using std::make_shared(). | ||
struct MakeSharedTag {}; |
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 is not only used by makeShared. It is more like a "CTorProtect" or "ProtectedTag"
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.
It is only needed for make_shared(). Otherwise the constructors could be hidden by making them private.
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 disagree. Redundantly repeating the type name of the type tag class where it is not needed is useless. It must only specified explicitly when using std::make_shared(), the only purpose it was introduced for.
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.
We hit often such discussion in our reviews. I think this is sourced by our different roles.
For me as a reviewer it is important to understand the code at once without looking into other references.
You as the author are more interested to avoid redundant statements.
Please keep in mind that the human reader is not a compiler which reads thousands of lines is a second, fetched form many included files. To make the code readable I prefer redundant statements more than explaining comments, because the compiler can check them.
A plain {} as a parameter can be everything, the reader needs to look up other references to understand it. This is the same for bool function parameter by the way.
Whenever the function signature changes, a {} will still compile even though it might be wrong.
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 plan was to use {}
everywhere but std::make_shared()
fails to deduce the type.
I don't see a reason to re-introduce the redundant type name in private forwards.
@@ -98,7 +98,8 @@ BeatsPointer BeatGrid::makeBeatGrid( | |||
// Calculate beat length as sample offsets | |||
const audio::FrameDiff_t beatLengthFrames = 60.0 * sampleRate / bpm.value(); | |||
|
|||
return BeatsPointer(new BeatGrid(sampleRate, subVersion, grid, beatLengthFrames)); | |||
return std::make_shared<BeatGrid>( | |||
MakeSharedTag{}, sampleRate, subVersion, grid, beatLengthFrames); |
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 think we should be explicit everywhere.
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.
LGTM, thank you.
I wanted to know it exactly and unfortunately your assumption is wrong that the compiler is able to optimize MakeSharedTag{} away. I have compiled the original version and a version where I have just deleted every MakeSharedTag{} coinsurance, than I have called: Here the interesting snippet.
the over all size of the .text segment changes form 0x1cd9 to 0x1ad1 this means 520 bytes of code saved. I have also checked if it makes a difference to the forwarded constructor if the tag is copied or if a new one is created. |
Conclusion: An empty struct is not optimized away if it is part of the public interface. I think that is different when it is used in header only templates. Here we protect the code against low risk of missused with a penalty at runtime. I don't think this will pay off as a general pattern. What is the bad think that may happen if one uses the constructor directly? |
...which doesn't invalidate any of my statements besides the wrong conclusion at runtime that doesn't matter. The type tag is only needed at compile time and should be treated as void at runtime. If you are keen on zero-cost abstractions I recommend switching to Rust instead of bothering with C++ ;) |
https://en.cppreference.com/w/cpp/memory/enable_shared_from_this/shared_from_this
Trading in a non-zero-sized, empty struct to catch errors at compile time for an exception at runtime would be a really bad deal. |
...and fix various inconsistencies and other flaws along the way.