Repository: fmtlib/fmt (2025-07-08T11:15:57Z)

Issue:
`fmt::basic_memory_buffer<...,std::pmr::polymorphic_allocator>` is not moveable
`std::pmr::polymorphic_allocator` is a non-propagating allocator: `std::allocator_traits<std::pmr::polymorphic_allocator>::propagate_on_container_move_assignment::value` is `false` and correspondingly, it has its move assignment operator deleted. 

Since `basic_memory_buffer::operator=(basic_memory_buffer&& other)` unconditionally calls

https://github.com/fmtlib/fmt/blob/40626af88bd7df9a5fb80be7b25ac85b122d6c21/include/fmt/format.h#L829-L830

for both propagating and non-propagating allocators, a snippet like this won't compile:

```c++
    std::pmr::unsynchronized_pool_resource r1;
    using pmr_memory_buffer = fmt::basic_memory_buffer<char,10,std::pmr::polymorphic_allocator<char>>;
    pmr_memory_buffer b1{ &r1 };
    pmr_memory_buffer b2{ &r1 };
    ......
    b2 = std::move( b1 );
```

A bit more practical snippet can be found here: [Compiler Explorer](https://www.godbolt.org/#g:!((g:!((g:!((h:codeEditor,i:(filename:'1',fontScale:14,fontUsePx:'0',j:1,lang:c%2B%2B,selection:(endColumn:1,endLineNumber:23,positionColumn:1,positionLineNumber:23,selectionStartColumn:1,selectionStartLineNumber:23,startColumn:1,startLineNumber:23),source:'%23include+%3Cfmt/format.h%3E%0A%23include+%3Cstring%3E%0A%23include+%3Cmemory_resource%3E%0A%0Aint+main()+%7B%0A++++//+Ensure+no+allocations+are+made+from+the+default+resource%0A++++std::pmr::set_default_resource(std::pmr::null_memory_resource())%3B%0A%0A++++std::pmr::unsynchronized_pool_resource+r1%3B%0A++++%0A++++using+pmr_memory_buffer+%3D+fmt::basic_memory_buffer%3Cchar,10,std::pmr::polymorphic_allocator%3Cchar%3E%3E%3B%0A+%0A++++pmr_memory_buffer+b%7B%26r1%7D%3B%0A++++std::pmr::string+s1+%7B+%22s1............................%22,+%26r1+%7D%3B%0A++++fmt::format_to(std::back_inserter(b),+%22s1:+%7B%7D%22,+s1)%3B%0A%0A++++std::pmr::vector%3Cpmr_memory_buffer%3E+bv+%7B%26r1%7D%3B%0A%0A++++//+This+does+not+compile+because+pmr::polymorphic_allocator+does+not+have+a+move+ctor%0A++++//+because+it!'s+a+non-propagaing+allocator%0A++++bv.push_back+(+std::move(b)+)%3B%0A%7D%0A'),l:'5',n:'0',o:'C%2B%2B+source+%231',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:executor,i:(argsPanelShown:'1',compilationPanelShown:'0',compiler:g151,compilerName:'',compilerOutShown:'0',execArgs:'',execStdin:'',fontScale:14,fontUsePx:'0',j:1,lang:c%2B%2B,libs:!((name:fmt,ver:trunk)),options:'',overrides:!(),runtimeTools:!(),source:1,stdinPanelShown:'1',wrap:'1'),l:'5',n:'0',o:'Executor+x86-64+gcc+15.1+(C%2B%2B,+Editor+%231)',t:'0')),header:(),k:50,l:'4',n:'0',o:'',s:0,t:'0')),l:'2',n:'0',o:'',t:'0')),version:4). It was inspired by the way `fmt::memory_buffer` class is used in [spdlog](https://github.com/gabime/spdlog). Its `ringbuffer_sink` [declares](https://github.com/gabime/spdlog/blob/287333ee00555aaece5a5cf6acc9040563c6f642/include/spdlog/sinks/ringbuffer_sink.h#L59) a circular buffer of basically `fmt::memory_buffer`s, and `move()`ing instances of `fmt::memory_buffer`s into it when they are ready.

I'm considering a fix where the move-propagating allocators will behave as before, whereas those that don't propagate on move will check whether the allocators (and the memory resources) are the same for the source and for the target. If source and target allocator instances are the same, `move()` can proceed as usual, passing ownership of the underlying buffer. If the allocators are different, the buffer is then copied. That would replicate the behaviour of standard library containers.


Discussion:
A PR to replicate the behavior of standard library containers is welcome.
I've submitted PR #4490. I'd appreciate it if you could take a moment to review it.



Patch:
diff --git a/include/fmt/format.h b/include/fmt/format.h
index 1cbd3df4e306..5ebc37499756 100644
--- a/include/fmt/format.h
+++ b/include/fmt/format.h
@@ -751,6 +751,14 @@ template <typename T> struct allocator : private std::decay<void> {
   }
 
   void deallocate(T* p, size_t) { std::free(p); }
+
+  FMT_CONSTEXPR20 friend bool operator==(allocator, allocator) noexcept {
+    return true;  // All instances of this allocator are equivalent.
+  }
+
+  FMT_CONSTEXPR20 friend bool operator!=(allocator a, allocator b) noexcept {
+    return !(a == b);
+  }
 };
 
 }  // namespace detail
@@ -826,11 +834,42 @@ class basic_memory_buffer : public detail::buffer<T> {
   FMT_CONSTEXPR20 ~basic_memory_buffer() { deallocate(); }
 
  private:
+  template <typename Alloc = Allocator>
+  FMT_CONSTEXPR20
+      typename std::enable_if<std::allocator_traits<Alloc>::
+                                  propagate_on_container_move_assignment::value,
+                              bool>::type
+      allocator_move_impl(basic_memory_buffer& other) {
+    alloc_ = std::move(other.alloc_);
+    return true;
+  }
+  // If the allocator does not propagate,
+  // then copy the content from source buffer.
+  template <typename Alloc = Allocator>
+  FMT_CONSTEXPR20
+      typename std::enable_if<!std::allocator_traits<Alloc>::
+                                  propagate_on_container_move_assignment::value,
+                              bool>::type
+      allocator_move_impl(basic_memory_buffer& other) {
+    T* data = other.data();
+    if (alloc_ != other.alloc_ && data != other.store_) {
+      size_t size = other.size();
+      // Perform copy operation, allocators are different
+      this->resize(size);
+      detail::copy<T>(data, data + size, this->data());
+      return false;
+    }
+    return true;
+  }
+
   // Move data from other to this buffer.
   FMT_CONSTEXPR20 void move(basic_memory_buffer& other) {
-    alloc_ = std::move(other.alloc_);
     T* data = other.data();
     size_t size = other.size(), capacity = other.capacity();
+    // Replicate the behaviour of std library containers
+    if (!allocator_move_impl(other)) {
+      return;
+    }
     if (data == other.store_) {
       this->set(store_, capacity);
       detail::copy<T>(other.store_, other.store_ + size, store_);


Test patch:
diff --git a/test/format-test.cc b/test/format-test.cc
index a303cf0dfa06..a835bd168311 100644
--- a/test/format-test.cc
+++ b/test/format-test.cc
@@ -307,7 +307,7 @@ TEST(memory_buffer_test, ctor) {
   EXPECT_EQ(123u, buffer.capacity());
 }
 
-using std_allocator = allocator_ref<std::allocator<char>>;
+using std_allocator = allocator_ref<std::allocator<char>, true>;
 
 TEST(memory_buffer_test, move_ctor_inline_buffer) {
   auto check_move_buffer =
@@ -351,6 +351,56 @@ TEST(memory_buffer_test, move_ctor_dynamic_buffer) {
   EXPECT_GT(buffer2.capacity(), 4u);
 }
 
+using std_allocator_n = allocator_ref<std::allocator<char>, false>;
+
+TEST(memory_buffer_test, move_ctor_inline_buffer_non_propagating) {
+  auto check_move_buffer =
+      [](const char* str,
+         basic_memory_buffer<char, 5, std_allocator_n>& buffer) {
+        std::allocator<char>* original_alloc_ptr = buffer.get_allocator().get();
+        const char* original_data_ptr = &buffer[0];
+        basic_memory_buffer<char, 5, std_allocator_n> buffer2(
+            std::move(buffer));
+        const char* new_data_ptr = &buffer2[0];
+        EXPECT_NE(original_data_ptr, new_data_ptr);
+        EXPECT_EQ(str, std::string(&buffer[0], buffer.size()));
+        EXPECT_EQ(str, std::string(&buffer2[0], buffer2.size()));
+        EXPECT_EQ(5u, buffer2.capacity());
+        // Allocators should NOT be transferred; they remain distinct instances.
+        // The original buffer's allocator pointer should still be valid (not
+        // nullptr).
+        EXPECT_EQ(original_alloc_ptr, buffer.get_allocator().get());
+        EXPECT_NE(original_alloc_ptr, buffer2.get_allocator().get());
+      };
+  auto alloc = std::allocator<char>();
+  basic_memory_buffer<char, 5, std_allocator_n> buffer(
+      (std_allocator_n(&alloc)));
+  const char test[] = "test";
+  buffer.append(string_view(test, 4));
+  check_move_buffer("test", buffer);
+  buffer.push_back('a');
+  check_move_buffer("testa", buffer);
+}
+
+TEST(memory_buffer_test, move_ctor_dynamic_buffer_non_propagating) {
+  auto alloc = std::allocator<char>();
+  basic_memory_buffer<char, 4, std_allocator_n> buffer(
+      (std_allocator_n(&alloc)));
+  const char test[] = "test";
+  buffer.append(test, test + 4);
+  const char* inline_buffer_ptr = &buffer[0];
+  buffer.push_back('a');
+  EXPECT_NE(&buffer[0], inline_buffer_ptr);
+  std::allocator<char>* original_alloc_ptr = buffer.get_allocator().get();
+  basic_memory_buffer<char, 4, std_allocator_n> buffer2;
+  buffer2 = std::move(buffer);
+  EXPECT_EQ(std::string(&buffer2[0], buffer2.size()), "testa");
+  EXPECT_GT(buffer2.capacity(), 4u);
+  EXPECT_NE(&buffer2[0], inline_buffer_ptr);
+  EXPECT_EQ(original_alloc_ptr, buffer.get_allocator().get());
+  EXPECT_NE(original_alloc_ptr, buffer2.get_allocator().get());
+}
+
 void check_move_assign_buffer(const char* str,
                               basic_memory_buffer<char, 5>& buffer) {
   basic_memory_buffer<char, 5> buffer2;
diff --git a/test/mock-allocator.h b/test/mock-allocator.h
index 32c4caae6a34..868cf6cb1ea4 100644
--- a/test/mock-allocator.h
+++ b/test/mock-allocator.h
@@ -37,7 +37,8 @@ template <typename T> class mock_allocator {
   MOCK_METHOD(void, deallocate, (T*, size_t));
 };
 
-template <typename Allocator> class allocator_ref {
+template <typename Allocator, bool PropagateOnMove = false>
+class allocator_ref {
  private:
   Allocator* alloc_;
 
@@ -48,6 +49,9 @@ template <typename Allocator> class allocator_ref {
 
  public:
   using value_type = typename Allocator::value_type;
+  using propagate_on_container_move_assignment =
+      typename std::conditional<PropagateOnMove, std::true_type,
+                                std::false_type>::type;
 
   explicit allocator_ref(Allocator* alloc = nullptr) : alloc_(alloc) {}
 
@@ -72,6 +76,19 @@ template <typename Allocator> class allocator_ref {
     return std::allocator_traits<Allocator>::allocate(*alloc_, n);
   }
   void deallocate(value_type* p, size_t n) { alloc_->deallocate(p, n); }
+
+  FMT_CONSTEXPR20 friend bool operator==(const allocator_ref& a,
+                                         const allocator_ref& b) noexcept {
+    if (a.alloc_ == b.alloc_) return true;
+    if (a.alloc_ == nullptr || b.alloc_ == nullptr) return false;
+
+    return *a.alloc_ == *b.alloc_;
+  }
+
+  FMT_CONSTEXPR20 friend bool operator!=(const allocator_ref& a,
+                                         const allocator_ref& b) noexcept {
+    return !(a == b);
+  }
 };
 
 #endif  // FMT_MOCK_ALLOCATOR_H_