Skip to content

Commit 0d14593

Browse files
authored
Handle allocator propagation in basic_memory_buffer::move, Fix fmtlib#4487 (fmtlib#4490)
* Handle allocator propagation in basic_memory_buffer::move 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. * Add test cases for the updated move ctor - 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
1 parent 8f3a965 commit 0d14593

File tree

3 files changed

+109
-3
lines changed

3 files changed

+109
-3
lines changed

include/fmt/format.h

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,14 @@ template <typename T> struct allocator : private std::decay<void> {
751751
}
752752

753753
void deallocate(T* p, size_t) { std::free(p); }
754+
755+
FMT_CONSTEXPR20 friend bool operator==(allocator, allocator) noexcept {
756+
return true; // All instances of this allocator are equivalent.
757+
}
758+
759+
FMT_CONSTEXPR20 friend bool operator!=(allocator a, allocator b) noexcept {
760+
return !(a == b);
761+
}
754762
};
755763

756764
} // namespace detail
@@ -826,11 +834,42 @@ class basic_memory_buffer : public detail::buffer<T> {
826834
FMT_CONSTEXPR20 ~basic_memory_buffer() { deallocate(); }
827835

828836
private:
837+
template <typename Alloc = Allocator>
838+
FMT_CONSTEXPR20
839+
typename std::enable_if<std::allocator_traits<Alloc>::
840+
propagate_on_container_move_assignment::value,
841+
bool>::type
842+
allocator_move_impl(basic_memory_buffer& other) {
843+
alloc_ = std::move(other.alloc_);
844+
return true;
845+
}
846+
// If the allocator does not propagate,
847+
// then copy the content from source buffer.
848+
template <typename Alloc = Allocator>
849+
FMT_CONSTEXPR20
850+
typename std::enable_if<!std::allocator_traits<Alloc>::
851+
propagate_on_container_move_assignment::value,
852+
bool>::type
853+
allocator_move_impl(basic_memory_buffer& other) {
854+
T* data = other.data();
855+
if (alloc_ != other.alloc_ && data != other.store_) {
856+
size_t size = other.size();
857+
// Perform copy operation, allocators are different
858+
this->resize(size);
859+
detail::copy<T>(data, data + size, this->data());
860+
return false;
861+
}
862+
return true;
863+
}
864+
829865
// Move data from other to this buffer.
830866
FMT_CONSTEXPR20 void move(basic_memory_buffer& other) {
831-
alloc_ = std::move(other.alloc_);
832867
T* data = other.data();
833868
size_t size = other.size(), capacity = other.capacity();
869+
// Replicate the behaviour of std library containers
870+
if (!allocator_move_impl(other)) {
871+
return;
872+
}
834873
if (data == other.store_) {
835874
this->set(store_, capacity);
836875
detail::copy<T>(other.store_, other.store_ + size, store_);

test/format-test.cc

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ TEST(memory_buffer_test, ctor) {
307307
EXPECT_EQ(123u, buffer.capacity());
308308
}
309309

310-
using std_allocator = allocator_ref<std::allocator<char>>;
310+
using std_allocator = allocator_ref<std::allocator<char>, true>;
311311

312312
TEST(memory_buffer_test, move_ctor_inline_buffer) {
313313
auto check_move_buffer =
@@ -351,6 +351,56 @@ TEST(memory_buffer_test, move_ctor_dynamic_buffer) {
351351
EXPECT_GT(buffer2.capacity(), 4u);
352352
}
353353

354+
using std_allocator_n = allocator_ref<std::allocator<char>, false>;
355+
356+
TEST(memory_buffer_test, move_ctor_inline_buffer_non_propagating) {
357+
auto check_move_buffer =
358+
[](const char* str,
359+
basic_memory_buffer<char, 5, std_allocator_n>& buffer) {
360+
std::allocator<char>* original_alloc_ptr = buffer.get_allocator().get();
361+
const char* original_data_ptr = &buffer[0];
362+
basic_memory_buffer<char, 5, std_allocator_n> buffer2(
363+
std::move(buffer));
364+
const char* new_data_ptr = &buffer2[0];
365+
EXPECT_NE(original_data_ptr, new_data_ptr);
366+
EXPECT_EQ(str, std::string(&buffer[0], buffer.size()));
367+
EXPECT_EQ(str, std::string(&buffer2[0], buffer2.size()));
368+
EXPECT_EQ(5u, buffer2.capacity());
369+
// Allocators should NOT be transferred; they remain distinct instances.
370+
// The original buffer's allocator pointer should still be valid (not
371+
// nullptr).
372+
EXPECT_EQ(original_alloc_ptr, buffer.get_allocator().get());
373+
EXPECT_NE(original_alloc_ptr, buffer2.get_allocator().get());
374+
};
375+
auto alloc = std::allocator<char>();
376+
basic_memory_buffer<char, 5, std_allocator_n> buffer(
377+
(std_allocator_n(&alloc)));
378+
const char test[] = "test";
379+
buffer.append(string_view(test, 4));
380+
check_move_buffer("test", buffer);
381+
buffer.push_back('a');
382+
check_move_buffer("testa", buffer);
383+
}
384+
385+
TEST(memory_buffer_test, move_ctor_dynamic_buffer_non_propagating) {
386+
auto alloc = std::allocator<char>();
387+
basic_memory_buffer<char, 4, std_allocator_n> buffer(
388+
(std_allocator_n(&alloc)));
389+
const char test[] = "test";
390+
buffer.append(test, test + 4);
391+
const char* inline_buffer_ptr = &buffer[0];
392+
buffer.push_back('a');
393+
EXPECT_NE(&buffer[0], inline_buffer_ptr);
394+
std::allocator<char>* original_alloc_ptr = buffer.get_allocator().get();
395+
basic_memory_buffer<char, 4, std_allocator_n> buffer2;
396+
buffer2 = std::move(buffer);
397+
EXPECT_EQ(std::string(&buffer2[0], buffer2.size()), "testa");
398+
EXPECT_GT(buffer2.capacity(), 4u);
399+
EXPECT_NE(&buffer2[0], inline_buffer_ptr);
400+
EXPECT_EQ(original_alloc_ptr, buffer.get_allocator().get());
401+
EXPECT_NE(original_alloc_ptr, buffer2.get_allocator().get());
402+
}
403+
354404
void check_move_assign_buffer(const char* str,
355405
basic_memory_buffer<char, 5>& buffer) {
356406
basic_memory_buffer<char, 5> buffer2;

test/mock-allocator.h

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ template <typename T> class mock_allocator {
3737
MOCK_METHOD(void, deallocate, (T*, size_t));
3838
};
3939

40-
template <typename Allocator> class allocator_ref {
40+
template <typename Allocator, bool PropagateOnMove = false>
41+
class allocator_ref {
4142
private:
4243
Allocator* alloc_;
4344

@@ -48,6 +49,9 @@ template <typename Allocator> class allocator_ref {
4849

4950
public:
5051
using value_type = typename Allocator::value_type;
52+
using propagate_on_container_move_assignment =
53+
typename std::conditional<PropagateOnMove, std::true_type,
54+
std::false_type>::type;
5155

5256
explicit allocator_ref(Allocator* alloc = nullptr) : alloc_(alloc) {}
5357

@@ -72,6 +76,19 @@ template <typename Allocator> class allocator_ref {
7276
return std::allocator_traits<Allocator>::allocate(*alloc_, n);
7377
}
7478
void deallocate(value_type* p, size_t n) { alloc_->deallocate(p, n); }
79+
80+
FMT_CONSTEXPR20 friend bool operator==(const allocator_ref& a,
81+
const allocator_ref& b) noexcept {
82+
if (a.alloc_ == b.alloc_) return true;
83+
if (a.alloc_ == nullptr || b.alloc_ == nullptr) return false;
84+
85+
return *a.alloc_ == *b.alloc_;
86+
}
87+
88+
FMT_CONSTEXPR20 friend bool operator!=(const allocator_ref& a,
89+
const allocator_ref& b) noexcept {
90+
return !(a == b);
91+
}
7592
};
7693

7794
#endif // FMT_MOCK_ALLOCATOR_H_

0 commit comments

Comments
 (0)