-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Handle allocator propagation in basic_memory_buffer::move, Fix #4487 #4490
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
base: master
Are you sure you want to change the base?
Conversation
Update `basic_memory_buffer::move` to respect `propagate_on_container_move_assignment`allocator trait. If the allocator should not propagate and differs from the target's allocator, fallback to copying the buffer instead of transferring ownership. This avoids potential allocator mismatch issues and ensures exception safety.
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.
Looks very good, definitely a move in the right direction. Please see my comments about C++11 compatibility, and a couple about more optimal buffer copying.
include/fmt/format.h
Outdated
T* data = other.data(); | ||
size_t size = other.size(), capacity = other.capacity(); | ||
if constexpr (alloc_traits::propagate_on_container_move_assignment::value) { |
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 constexpr
is a C++17 feature, and {fmt} compatibility depth is C++11, I believe. However std::pmr::polymorphic_allocator
(which is the primary case for the non-propagating allocators problem) is a C++17 feature as well. So I think it's ok to limit this entire new code block to C++17 (__cpp_if_constexpr
might have been the right feature guard to check, but I'm concerned of older C++17 compilers not supporting feature guards yet).
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's better to avoid conditional compilation if possible. Can we just use if
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 guess if
will have the same issue as reported in #4487.
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 we just use
if
here?
Nope, it will require move assignment operator from the allocator to compile in all cases then.
I guess if will have the same issue as reported in #4487.
Exactly.
Another option is to fall back to the old school SFINAE monsters, and conditional compilation looks like a lesser evil to me. @vitaut WDYT?
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 recommend using compiler-specific if constexpr capability detection, as SFINAE introduces unnecessary logical complexity and a regular if statement won't compile in this context.
How about adding this snippet to base.h
?
// Detect C++17 if constexpr support
#ifdef FMT_USE_CONSTEXPR17
// Use the provided definition.
#elif defined(__cpp_if_constexpr) && __cpp_if_constexpr >= 201606L
// Standard feature test macro for if constexpr
# define FMT_USE_CONSTEXPR17 1
#elif FMT_GCC_VERSION >= 700 && FMT_CPLUSPLUS >= 201703L
# define FMT_USE_CONSTEXPR17 1 // GCC 7.0+ with C++17 mode
#elif FMT_CLANG_VERSION >= 309 && FMT_CPLUSPLUS >= 201703L
# define FMT_USE_CONSTEXPR17 1 // Clang 3.9+ with C++17 mode
#elif FMT_MSC_VERSION >= 1911 && FMT_CPLUSPLUS >= 201703L
# define FMT_USE_CONSTEXPR17 1 // MSVC 2017 15.3+ with C++17 mode
#elif FMT_ICC_VERSION >= 1810 && FMT_CPLUSPLUS >= 201703L
# define FMT_USE_CONSTEXPR17 1 // Intel C++ 18.1+ with C++17 mode
#elif FMT_HAS_FEATURE(cxx_if_constexpr)
# define FMT_USE_CONSTEXPR17 1 // Clang feature detection
#else
# define FMT_USE_CONSTEXPR17 0
#endif
#if FMT_USE_CONSTEXPR17
# define FMT_CONSTEXPR17 if constexpr
#else
# define FMT_CONSTEXPR17 if
#endif
source: cppreference
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 SFINAE shouldn't be too complex, definitely better than introducing FMT_USE_CONSTEXPR17
.
include/fmt/format.h
Outdated
alloc_ = std::move(other.alloc_); | ||
} else { | ||
if (alloc_ != other.alloc_) { | ||
this->reserve(capacity); |
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 might be costly to propagate the entire capacity when copying. What if the source buffer used to be 10k, but then shrunk in size to 1k. Its capacity is 10k still. But is there any point in allocating 10k at the target when the actual data to be copied is 1k?
include/fmt/format.h
Outdated
this->reserve(capacity); | ||
detail::copy<T>(data, data + size, this->data()); | ||
this->resize(size); | ||
return; |
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 branch needs to handle the case when data == other.store_
as well. Otherwise the store_
buffer optimization is bypassed for the target buffer.
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.
You're right.
Also, do we need to clear other
if data != other.store_
?
i.e.
other.set(other.store_, 0);
other.clear();
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 would say no. Nothing in the standard requires move assignment operator argument to be in a certain state after a successful call, empty or whatever.
include/fmt/format.h
Outdated
@@ -746,6 +746,14 @@ template <typename T> struct allocator : private std::decay<void> { | |||
} | |||
|
|||
void deallocate(T* p, size_t) { std::free(p); } | |||
|
|||
friend bool operator==(const allocator&, const allocator&) noexcept { |
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.
Let's pass allocator
by value.
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.
Thanks for the PR. Please add a test case and address inline comments.
include/fmt/format.h
Outdated
@@ -747,12 +747,12 @@ template <typename T> struct allocator : private std::decay<void> { | |||
|
|||
void deallocate(T* p, size_t) { std::free(p); } | |||
|
|||
friend bool operator==(const allocator&, const allocator&) noexcept { | |||
return true; // All instances of this allocator are equivalent. | |||
friend bool operator==(const allocator, const allocator) noexcept { |
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.
const
is not needed here and below
- Added two test cases `move_ctor_inline_buffer_non_propagating` and `move_ctor_dynamic_buffer_non_propagating` - Added `PropageteOnMove` template parameter to `allocator_ref` class to be compatible with the old test cases - `allocator_ref` now implements `!=` and `==` operators
35b131e
to
764fca9
Compare
buffer.push_back('a'); | ||
check_move_buffer("testa", buffer); |
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.
How is this different from the first check? Can we have a single check and get rid of the lambda?
// Move data from other to this buffer. | ||
FMT_CONSTEXPR20 void move(basic_memory_buffer& other) { | ||
alloc_ = std::move(other.alloc_); | ||
using alloc_traits = std::allocator_traits<Allocator>; |
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.
Looks unused.
Fixes #4487.
This PR implements allocator-aware move behavior in
basic_memory_buffer
, following the suggestion by @dlex in the linked issue.To enable allocator comparison (
alloc_ == other.alloc_
), I addedoperator==
andoperator!=
to the following classes:allocator_ref
allocator
Summary of Changes
std::allocator_traits<>::propagate_on_container_move_assignment
.Test Failures
There are currently two failing tests (Test 7/21) that may need to be updated to match the new move semantics:
move_ctor_inline_buffer
move_ctor_dynamic_buffer
I propose updating the above tests to align with the allocator propagation semantics. However, before proceeding, I’d like to get feedback from maintainers and reviewers. @vitaut
Do these changes and the proposed test adjustments align with the intended design of
basic_memory_buffer
?