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

Beats: Use std::make_shared/shared_from_this() #4336

Merged
merged 7 commits into from
Oct 1, 2021
Merged

Beats: Use std::make_shared/shared_from_this() #4336

merged 7 commits into from
Oct 1, 2021

Conversation

uklotzde
Copy link
Contributor

...and fix various inconsistencies and other flaws along the way.

@tcoyvwac
Copy link
Contributor

Was about to PR the same thing. 😄

@uklotzde
Copy link
Contributor Author

@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.

@uklotzde
Copy link
Contributor Author

uklotzde commented Sep 29, 2021

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.

@uklotzde
Copy link
Contributor Author

In the long term we need to think about applying the same principles to other areas like Cues and the bloated Track class with their error prone internal locking. There we are facing additional challenges like how to embed the design into connected QObjects in the UI layer. The current design is a big mess.

@daschuer
Copy link
Member

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.
We need to skip the implementation of copy-on-write and have our immutable object ready. This comes with the same thread safety Qt has, when sending it via signals and slot connections.

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?

@uklotzde
Copy link
Contributor Author

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.

@uklotzde
Copy link
Contributor Author

If you have ideas that extend the scope of this PR then please provide a follow-up PR for review.

@tcoyvwac
Copy link
Contributor

tcoyvwac commented Sep 30, 2021

@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.

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);
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 you use here the explicit version, where above you use only the braced tantalizer?

Copy link
Contributor Author

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.

Copy link
Member

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.

BeatGrid::BeatGrid(
MakeSharedTag,
const BeatGrid& other)
: BeatGrid({}, other, other.m_grid, other.m_beatLengthFrames) {
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

@daschuer daschuer left a 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.

public:
virtual ~Beats() = default;

BeatsPointer clone() const {
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Member

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 {};
Copy link
Member

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"

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 is only needed for make_shared(). Otherwise the constructors could be hidden by making them private.

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 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.

Copy link
Member

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.

Copy link
Contributor Author

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);
Copy link
Member

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.

Copy link
Member

@daschuer daschuer left a comment

Choose a reason for hiding this comment

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

LGTM, thank you.

@daschuer daschuer merged commit 8b1ed1b into mixxxdj:main Oct 1, 2021
@daschuer
Copy link
Member

daschuer commented Oct 1, 2021

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:
objdump -S build/Debug/CMakeFiles/mixxx-lib.dir/src/track/beatgrid.cpp.o > beatgid.cpp

Here the interesting snippet.
Original:

    return std::make_shared<BeatGrid>(MakeSharedTag{}, *this, grid, m_beatLengthFrames);
}
    1575:	48 8b 44 24 38       	mov    0x38(%rsp),%rax
    157a:	64 48 33 04 25 28 00 	xor    %fs:0x28,%rax
    1581:	00 00 
    1583:	75 6f                	jne    15f4 <_ZNK5mixxx8BeatGrid9translateEd+0x2a4>
    1585:	48 83 c4 48          	add    $0x48,%rsp
    1589:	4c 89 e0             	mov    %r12,%rax
    158c:	5b                   	pop    %rbx
    158d:	5d                   	pop    %rbp
    158e:	41 5c                	pop    %r12
    1590:	41 5d                	pop    %r13
    1592:	41 5e                	pop    %r14
    1594:	41 5f                	pop    %r15
    1596:	c3                   	retq   
    1597:	66 0f 1f 84 00 00 00 	nopw   0x0(%rax,%rax,1)
    159e:	00 00 
  return p != NULL ? *p : *reinterpret_cast<const ::mixxx::track::io::Bpm*>(

    return std::make_shared<BeatGrid>(*this, grid, m_beatLengthFrames);
}
    1430:	48 8b 44 24 38       	mov    0x38(%rsp),%rax
    1435:	64 48 33 04 25 28 00 	xor    %fs:0x28,%rax
    143c:	00 00 
    143e:	75 54                	jne    1494 <_ZNK5mixxx8BeatGrid9translateEd+0x234>
    1440:	48 83 c4 40          	add    $0x40,%rsp
    1444:	4c 89 e0             	mov    %r12,%rax
    1447:	5b                   	pop    %rbx
    1448:	5d                   	pop    %rbp
    1449:	41 5c                	pop    %r12
    144b:	41 5d                	pop    %r13
    144d:	41 5e                	pop    %r14
    144f:	c3                   	retq   
  return p != NULL ? *p : *reinterpret_cast<const ::mixxx::track::io::Bpm*>(

nopw 0x0(%rax,%rax,1) is the 0 initializing "constructor" of the MakeSharedTag which can not be optimized away.

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.
Result: Both is exactly the same

@daschuer
Copy link
Member

daschuer commented Oct 1, 2021

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?

@uklotzde
Copy link
Contributor Author

uklotzde commented Oct 1, 2021

...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++ ;)

@uklotzde
Copy link
Contributor Author

uklotzde commented Oct 1, 2021

https://en.cppreference.com/w/cpp/memory/enable_shared_from_this/shared_from_this

Otherwise the behavior is undefined (until C++17)std::bad_weak_ptr is thrown (by the shared_ptr constructor from a default-constructed weak_this) (since C++17).

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.

@uklotzde uklotzde deleted the beats branch October 1, 2021 08:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants