[PATCH] C++: explicit consuming relocation for mpz_class

Roberto Bagnara roberto.bagnara at gmail.com
Wed Sep 30 08:43:12 CEST 2026


Dear GMP developers,

I would like to propose an addition to GMP's C++ interface: explicit
consuming relocation of mpz_class objects, including operations on
whole ranges. The attached gmp-6.3.0-relocation.patch is against GMP
6.3.0 and includes documentation and a regression test in GMP's own
test suite.

The intended users are authors of containers that manage object
storage and lifetimes explicitly. Such containers need to grow arrays
of integers, compact storage, or shift a range to make or close a
gap. They often need to preserve the integer values and their existing
limb allocations while discarding or reusing the old object slots. A
GMP-supported operation would let them express this directly, without
duplicating assumptions about the representation and ownership of
mpz_class in application code.

Ordinary mpz_class moves already transfer ownership without copying
limbs.  The distinction here is the source contract: a move leaves a
source that can be destroyed normally, whereas these containers can
relinquish the source slots entirely. An explicit consuming operation
need not reset those slots or arrange for their subsequent
destruction. For ranges, it also allows GMP to implement the transfer
using a bulk descriptor copy or move, including overlapping shifts,
instead of requiring callers to express it as a sequence of ordinary
object moves. This is an additional facility, not a change to the
existing copy, move or destruction semantics.

The patch proposes four static members, available in C++11 and later:

   mpz_class::relocate_into(to, from) Relocate one integer into
     distinct destination storage.

   mpz_class::relocate_range(to, from, count) Relocate a range into
     disjoint destination storage.

   mpz_class::relocate_shift_right(from, count, shift)
   mpz_class::relocate_shift_left(from, count, shift) Relocate a range
   within one allocation, allowing overlap.

All four are noexcept. They transfer the existing limb allocations
without allocating, reallocating, freeing or copying limbs. Their work
depends on the number of integer descriptors, independently of the
integers' sizes.  For example, a custom array can grow by allocating
destination storage, relocating its live range, and releasing the old
storage without invoking the consumed source objects'
destructors. Overlapping operations allow the corresponding insertion,
erasure and compaction implementations.  Existing standard containers
would not automatically use these functions.

The ownership contract is deliberately explicit. Source-only slots are
consumed: they must not be read as integers, assigned to, or
destroyed.  The caller must either reconstruct an mpz_class there or
release the storage without those destructors. In particular, an
ordinary local variable must be reconstructed before its implicit
destructor runs.  Consumption transfers ownership; it does not itself
end the source object's C++ lifetime. With overlapping ranges, slots
in the intersection remain live and hold their new destination values.

Destination-only slots must be suitably aligned unconstructed or
consumed storage; any existing owned value must first be
destroyed. The manual specifies the complete range, overlap and size
requirements, excludes base-class subobjects, and requires valid
nonnull storage even for zero-count or zero-shift calls. It includes
an allocation/relocation/ destruction example and explains how to
reuse consumed slots.

The implementation establishes destination object lifetimes using
placement new with private constructors before transferring descriptor
representations.  For overlapping operations, only destination-only
objects are constructed.  Compile-time checks enforce the
representation properties used by the bulk operations: a
standard-layout, descriptor-only wrapper and a trivially
default-constructible, trivially copyable descriptor. The argument
relies on copying the representations of live descriptor subobjects;
it does not treat mpz_class itself as trivially copyable. It requires
neither C++20 implicit object creation nor compiler-specific
relocation extensions.

At -O2 in C++17 mode, the four primitives produced the same
instruction bodies as their direct descriptor-transfer baselines with
Apple Clang 17, GCC 16 (a development snapshot) and GCC 12.2 on the
tested macOS/AArch64, Ubuntu/x86_64 and Debian/AArch64 systems,
respectively. This supports keeping the cost of the low-level
transfers while giving them an explicit lifetime and ownership
contract, not a claim of a speedup over every implementation using
ordinary moves. Unoptimized builds can retain the empty construction
loops; the allocation and ownership guarantees do not depend on
optimization.

The GMP test suite completes without failures on all three systems,
with one expected skip per run. The added regression checks complete
values and limb ownership, zeros and large signed values, destination
reuse, source reconstruction, overlap in both directions, boundary and
no-op cases, and ordinary copy/move behavior. It counts allocation,
reallocation and free calls while retaining GMP's native test
allocator checks. In additional macOS validation, the C++ regression
and inline operations pass ASan/UBSan; the underlying GMP C library
was not sanitizer-instrumented. Installed consumers were tested there
in C++11, C++17 and C++20 modes, with separate compile-only checks for
C++98 and disabled C++11 features.  Windows has not been tested.

The class layout is unchanged, but its definition changes. The
proposal therefore includes a feature macro and an optional
configuration link check against a marker in libgmpxx; relocation does
not call that marker. These checks help detect mismatched headers and
libraries, but do not replace rebuilding and using the relevant C++
components consistently. The names, API shape and detection convention
are all open for discussion.

The patch includes the generated tests/cxx/Makefile.in changes so it
can also be applied to the release tarball without regenerating the
build machinery. Those generated-file hunks can be omitted when
integrating the change into your development tree.

I would welcome your review of whether this facility belongs in GMP's
C++ interface, and in particular of the lifetime argument and the
consuming-source contract.

Kind regards,

     Roberto
-------------- next part --------------
--- a/gmpxx.h
+++ b/gmpxx.h
@@ -33,6 +33,8 @@
 #define __GMP_PLUSPLUS__
 
 #include <iosfwd>
+#include <cstddef>
+#include <new>
 
 #include <cstring>  /* for strlen */
 #include <limits>  /* numeric_limits */
@@ -64,9 +66,17 @@
 
 #if __GMPXX_USE_CXX11
 #define __GMPXX_NOEXCEPT noexcept
-#include <type_traits> // for common_type
+#define __GMPXX_HAVE_MPZ_RELOCATION 2
+#include <type_traits> // for common_type and relocation checks
 #else
 #define __GMPXX_NOEXCEPT
+#define __GMPXX_HAVE_MPZ_RELOCATION 0
+#endif
+
+// This link-time marker identifies libgmpxx built with revision 2 of the
+// relocation-enabled class definition.  It is not called by relocation.
+#if __GMPXX_HAVE_MPZ_RELOCATION
+extern "C" __GMP_DECLSPEC_XX void __gmpxx_mpz_relocation_v2 (void);
 #endif
 
 // Max allocations for plain types when converted to GMP types
@@ -1576,6 +1586,28 @@
   typedef mpz_t value_type;
   value_type mp;
 
+#if __GMPXX_USE_CXX11
+  // Begin the wrapper and descriptor lifetimes without initializing their
+  // scalar fields.  Only relocation may use this constructor: it must supply
+  // a valid descriptor before any value is read or the object is destroyed.
+  struct __relocation_uninitialized_tag {};
+  __gmp_expr(__relocation_uninitialized_tag) noexcept
+  {
+    static_assert(std::is_trivially_default_constructible<value_type>::value,
+                  "mpz relocation requires trivial descriptor construction");
+    static_assert(std::is_trivially_copyable<value_type>::value,
+                  "mpz relocation requires a trivially copyable descriptor");
+    static_assert(std::is_standard_layout<__gmp_expr>::value,
+                  "mpz relocation requires a standard-layout wrapper");
+    static_assert(sizeof(__gmp_expr) == sizeof(value_type),
+                  "mpz relocation requires a descriptor-only wrapper");
+  }
+
+  struct __relocation_tag {};
+  __gmp_expr(__relocation_tag, __gmp_expr& from) noexcept
+  { *mp = *from.mp; }
+#endif
+
   // Helper functions used for all arithmetic types
   void assign_ui(unsigned long l)
   {
@@ -1626,6 +1658,68 @@
   }
 
 public:
+#if __GMPXX_USE_CXX11
+  // Transfer ownership into raw or previously consumed destination storage.
+  // A consumed source must not be accessed or destroyed: either reconstruct
+  // an object of this type in its storage or release the storage without
+  // destroying it.  In particular, reconstruct before any implicit destructor
+  // call.  Consumption does not itself end the source object's lifetime.
+  // See the manual's Integer Relocation section for the complete contract.
+  static __gmp_expr* relocate_into(__gmp_expr* to, __gmp_expr* from) noexcept
+  {
+    return ::new (static_cast<void*>(to))
+      __gmp_expr(__relocation_tag(), *from);
+  }
+
+  // The ranges must be disjoint.  All destination slots must be raw or
+  // consumed storage, and all source slots must hold live, owned values.
+  // Pointers must designate valid, nonnull array storage even for count zero.
+  static void relocate_range(__gmp_expr* to, __gmp_expr* from,
+                             std::size_t count) noexcept
+  {
+    for (std::size_t i = 0; i != count; ++i)
+      ::new (static_cast<void*>(to + i))
+        __gmp_expr(__relocation_uninitialized_tag());
+
+    // Each wrapper's only state is its live, trivially copyable descriptor.
+    // Copying the range copies each descriptor to the corresponding live
+    // descriptor subobject.  No array of descriptors is fabricated here.
+    // The empty construction loop can disappear without changing this
+    // lifetime argument.  Reassess it if the wrapper representation changes.
+    std::memcpy(static_cast<void*>(to), static_cast<const void*>(from),
+                count * sizeof(__gmp_expr));
+  }
+
+  // Shift within one array allocation.  Only destination-only slots may be
+  // raw or consumed.  Preserve overlapping source lifetimes until memmove;
+  // the source-only prefix is consumed by a right shift, the suffix by a
+  // left shift.  Count or shift zero leaves values unchanged, but pointers
+  // and pointer arithmetic must still designate valid, nonnull array storage.
+  static void relocate_shift_right(__gmp_expr* from, std::size_t count,
+                                   std::size_t shift) noexcept
+  {
+    const std::size_t first = shift < count ? count : shift;
+    const std::size_t construct = shift < count ? shift : count;
+    for (std::size_t i = 0; i != construct; ++i)
+      ::new (static_cast<void*>(from + first + i))
+        __gmp_expr(__relocation_uninitialized_tag());
+    std::memmove(static_cast<void*>(from + shift),
+                 static_cast<const void*>(from),
+                 count * sizeof(__gmp_expr));
+  }
+
+  static void relocate_shift_left(__gmp_expr* from, std::size_t count,
+                                  std::size_t shift) noexcept
+  {
+    __gmp_expr* const to = from - shift;
+    const std::size_t construct = shift < count ? shift : count;
+    for (std::size_t i = 0; i != construct; ++i)
+      ::new (static_cast<void*>(to + i))
+        __gmp_expr(__relocation_uninitialized_tag());
+    std::memmove(static_cast<void*>(to), static_cast<const void*>(from),
+                 count * sizeof(__gmp_expr));
+  }
+#endif
   mp_bitcnt_t get_prec() const { return mpf_get_default_prec(); }
 
   // constructors and destructor
--- a/cxx/limits.cc
+++ b/cxx/limits.cc
@@ -30,6 +30,13 @@
 
 #include "gmpxx.h"
 
+#if __GMPXX_HAVE_MPZ_RELOCATION
+extern "C" __GMP_DECLSPEC_XX void
+__gmpxx_mpz_relocation_v2 (void)
+{
+}
+#endif
+
 namespace std {
 #define GMPXX_INSTANTIATE_LIMITS(T) \
   const bool numeric_limits<T>::is_specialized; \
--- a/doc/gmp.texi
+++ b/doc/gmp.texi
@@ -7014,6 +7014,166 @@
 Conversions between @code{mpz_class} and @code{double}, however, are defined
 to follow the corresponding C functions @code{mpz_get_d} and @code{mpz_set_d}.
 And comparisons are always made exactly, as per @code{mpz_cmp_d}.
+
+ at anchor{C++ Integer Relocation}
+ at subheading Integer Relocation
+
+ at cindex Relocation, C++ integers
+ at cindex Consumed objects, C++ integers
+The following static member functions transfer integer values into other
+storage without copying their limbs.  They are intended for containers which
+manage object construction and destruction explicitly.  Unlike an ordinary
+C++ move, relocation @dfn{consumes} source objects which do not also belong
+to the destination range.  A consumed object must not be accessed as an
+integer, assigned to, or destroyed.  Its storage can instead be reused by
+constructing a new @code{mpz_class} there, or released without calling the
+consumed object's destructor.
+
+Consumption is an ownership state, not an operation which ends the source
+object's C++ lifetime.  Reusing or releasing its storage subsequently ends
+that lifetime.  Before any implicit destructor call, including destruction
+by an enclosing object or container, a new @code{mpz_class} must have been
+constructed in the consumed object's place.  In particular, relocating an
+ordinary local variable without reconstructing it before leaving its scope
+is invalid.  Use ordinary move construction or move assignment when the
+source must remain usable and destructible.
+
+ at defmac __GMPXX_HAVE_MPZ_RELOCATION
+Defined by @file{gmpxx.h} to 2 when revision 2 of these functions is available,
+and to 0
+otherwise.  They require C++11 or later and are unavailable when the C++11
+features of @file{gmpxx.h} have been disabled.
+ at end defmac
+
+ at deftypefun void __gmpxx_mpz_relocation_v2 (void)
+This function does nothing.  Its presence in @file{libgmpxx} identifies a
+library built with revision 2 of the relocation-enabled @code{mpz_class}
+definition.  Its declaration and definition are available only when
+ at code{__GMPXX_HAVE_MPZ_RELOCATION} is 2.  It is intended for configuration
+link checks; the relocation operations do not call it.
+ at end deftypefun
+
+Use matching @file{gmpxx.h} definitions when building @file{libgmpxx} and
+its C++ clients and dependencies.  The relocation functions do not change
+the class layout, but they do change its definition.  Installing this
+header over an existing library is therefore insufficient: rebuild
+ at file{libgmpxx} and the relevant C++ components consistently.  A link check
+for the marker can detect some mismatched installations, but does not
+verify the definitions used by every translation unit or the libraries
+selected by the runtime loader.
+
+For every call, including calls with zero @var{count} or @var{shift}, the
+following requirements apply.
+
+ at itemize @bullet
+ at item
+Slots must not be base-class subobjects.  Reconstructing a base-class
+subobject does not provide the transparent replacement required by this
+contract for later use or destruction of its enclosing object.
+
+ at item
+Every source slot holds a live, non-const @code{mpz_class} with an ordinary,
+unconsumed value.
+
+ at item
+Every destination slot outside the source range is suitably aligned storage
+for an @code{mpz_class}.  It is either unconstructed storage or a previously
+consumed @code{mpz_class} slot.  In particular, relocation does not destroy an
+existing destination value; the caller must destroy such a value first.
+
+ at item
+For range operations, the pointers are nonnull and the pointers and counts
+describe valid array storage, such as storage obtained from
+ at code{std::allocator<mpz_class>}.  All pointer
+arithmetic must stay within the appropriate array allocation or reach its
+one-past-the-end pointer.  The byte count
+ at code{@var{count} * sizeof(mpz_class)} must be representable in
+ at code{std::size_t}.
+ at end itemize
+
+The functions transfer ownership of the limb allocations to the destination
+objects.  They do not allocate, reallocate, free, or copy limbs, and do not
+call the memory allocation functions described in @ref{Custom Allocation}.
+All four functions are declared @code{noexcept}.  Their work is proportional
+to the number of integers transferred, independently of the integers'
+sizes.  The source-only slots are not reset to zero.
+
+Optimizing compilers can eliminate the empty destination-construction loops
+and retain the descriptor transfer alone.  Unoptimized builds may execute
+those loops and be slower than direct byte transfers.  The allocation and
+ownership guarantees above apply independently of compiler optimization.
+
+Pointers and references to the source wrappers or their @code{mpz_t}
+descriptors are not redirected to the destination.  Obtain new pointers or
+references from the destination to access the transferred integers.  With
+overlapping ranges, a slot in both ranges instead holds its new destination
+value and remains usable and destructible.
+
+ at deftypefun {mpz_class *} mpz_class::relocate_into (mpz_class *@var{to}, mpz_class *@var{from})
+Construct an integer at @var{to}, transferring the value and limb ownership
+of @code{*@var{from}}, and return a pointer to the constructed object.
+The two slots must be distinct and nonoverlapping; neither pointer can be
+null.  The source is consumed.
+ at end deftypefun
+
+ at deftypefun void mpz_class::relocate_range (mpz_class *@var{to}, mpz_class *@var{from}, std::size_t @var{count})
+Transfer @var{count} integers from the range beginning at @var{from} to
+unconstructed or consumed slots beginning at @var{to}, preserving their
+order.  The ranges must not overlap.  All @var{count} source objects are
+consumed.
+
+If @var{count} is zero, no values are transferred or consumed.  Both
+pointers must still designate valid nonnull array storage, as required for
+ at code{memcpy}.
+ at end deftypefun
+
+ at deftypefun void mpz_class::relocate_shift_right (mpz_class *@var{from}, std::size_t @var{count}, std::size_t @var{shift})
+ at deftypefunx void mpz_class::relocate_shift_left (mpz_class *@var{from}, std::size_t @var{count}, std::size_t @var{shift})
+Transfer @var{count} integers beginning at @var{from} to the range beginning
+at @code{@var{from} + @var{shift}} or @code{@var{from} - @var{shift}},
+respectively, preserving their order.  Both ranges must be within the same
+array allocation, with sufficient storage in the indicated direction.
+The @var{shift} argument is a count of @code{mpz_class} slots, not bytes.
+The ranges may overlap, and @var{shift} need not be less than @var{count}.
+
+Only destination slots outside the source range are constructed.  Slots in
+both ranges retain their object lifetimes and receive their new values.
+For a right shift, the first @code{min(@var{count}, @var{shift})} source
+slots are consumed; for a left shift, the last that many source slots are
+consumed.  Storage outside both ranges is unchanged, including any gap
+between disjoint ranges.  All slot offsets used must be representable
+without arithmetic overflow.
+
+If either @var{count} or @var{shift} is zero, all values are unchanged and
+no source objects are consumed.  @var{from} must still be nonnull and the
+pointer arithmetic defining the destination range must be valid, as
+required for @code{memmove}.
+ at end deftypefun
+
+For example, the following allocates storage for two integers, constructs
+one, and relocates it into the other slot.  Only the destination is destroyed
+before the allocation is released.  In addition to @file{gmpxx.h}, this
+example requires @file{memory} and @file{new}.
+
+ at example
+std::allocator<mpz_class> allocator;
+mpz_class *storage = allocator.allocate (2);
+::new (static_cast<void *> (storage)) mpz_class (42);
+mpz_class *result = mpz_class::relocate_into (storage + 1, storage);
+// *result is 42; storage[0] is consumed.
+result->~mpz_class ();
+allocator.deallocate (storage, 2);
+ at end example
+
+To reuse a consumed source slot instead, construct a new @code{mpz_class}
+there with placement @code{new}, without first calling its destructor.
+Default construction creates a new zero integer and makes the slot usable
+and destructible again.
+
+A container releasing an allocation must still destroy every unconsumed
+object remaining in that allocation before releasing its storage.  Using
+ at code{delete} or @code{delete[]} directly on storage containing consumed
+objects would call their destructors and is therefore invalid.
 
 
 @node C++ Interface Rationals, C++ Interface Floats, C++ Interface Integers, C++ Class Interface
--- a/tests/cxx/Makefile.am
+++ b/tests/cxx/Makefile.am
@@ -38,7 +38,7 @@
   t-ops t-ops2qf t-ops2f t-ops3 t-ostream t-prec \
   t-ternary t-unary \
   t-do-exceptions-work-at-all-with-this-compiler \
-  t-ops2z t-assign t-constr t-rand
+  t-ops2z t-assign t-constr t-rand t-relocate
 
 TESTS = $(check_PROGRAMS)
 endif
@@ -64,6 +64,7 @@
 t_ostream_SOURCES = t-ostream.cc
 t_prec_SOURCES    = t-prec.cc
 t_rand_SOURCES    = t-rand.cc
+t_relocate_SOURCES= t-relocate.cc
 t_ternary_SOURCES = t-ternary.cc
 t_unary_SOURCES   = t-unary.cc
 t_do_exceptions_work_at_all_with_this_compiler_SOURCES = \
--- /dev/null
+++ b/tests/cxx/t-relocate.cc
@@ -0,0 +1,407 @@
+/* Test destructive relocation of mpz_class objects.
+
+Copyright (C) 2026 BUGSENG srl (http://bugseng.com)
+
+This file is part of the GNU MP Library test suite.
+
+The GNU MP Library test suite is free software; you can redistribute it
+and/or modify it under the terms of the GNU General Public License as
+published by the Free Software Foundation; either version 3 of the License,
+or (at your option) any later version.
+
+The GNU MP Library test suite is distributed in the hope that it will be
+useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
+Public License for more details.
+
+You should have received a copy of the GNU General Public License along with
+the GNU MP Library test suite.  If not, see https://www.gnu.org/licenses/.  */
+
+#include "config.h"
+
+#include "gmpxx.h"
+#include "gmp-impl.h"
+#include "tests.h"
+
+#if __GMPXX_USE_CXX11
+
+#if !defined (__GMPXX_HAVE_MPZ_RELOCATION) || __GMPXX_HAVE_MPZ_RELOCATION != 2
+#error mpz_class relocation must be available in C++11 and later
+#endif
+
+#include <algorithm>
+#include <memory>
+#include <new>
+#include <utility>
+#include <vector>
+
+static size_t allocations, reallocations, releases;
+
+/* Keep the native test allocator's redzones, size checks, bad-free checks
+   and final leak check; only add counters around it.  */
+static void *
+count_allocate (size_t n)
+{
+  ++allocations;
+  return tests_allocate (n);
+}
+
+static void *
+count_reallocate (void *p, size_t old_size, size_t new_size)
+{
+  ++reallocations;
+  return tests_reallocate (p, old_size, new_size);
+}
+
+static void
+count_free (void *p, size_t n)
+{
+  ++releases;
+  tests_free (p, n);
+}
+
+static void
+reset_counts ()
+{
+  allocations = reallocations = releases = 0;
+}
+
+static void
+check_no_memory_operations ()
+{
+  ASSERT_ALWAYS (allocations == 0);
+  ASSERT_ALWAYS (reallocations == 0);
+  ASSERT_ALWAYS (releases == 0);
+}
+
+/* Include both an unallocated zero and a zero retaining a limb allocation.
+   Use different low limbs and high-bit positions to detect reordering and
+   compare the complete numeric value, not just size or selected bits.  */
+static void
+set_value (mpz_class &z, size_t i)
+{
+  if (i % 4 != 0)
+    {
+      mpz_set_ui (z.get_mpz_t (), i + 23);
+      mpz_setbit (z.get_mpz_t (), 65537 + i);
+      if (i % 4 == 2)
+        mpz_neg (z.get_mpz_t (), z.get_mpz_t ());
+      if (i % 4 == 3)
+        mpz_set_ui (z.get_mpz_t (), 0);
+    }
+}
+
+struct saved_value
+{
+  mpz_class value;
+  mp_ptr limbs;
+  int allocated, size;
+
+  saved_value (const mpz_class &z, size_t i)
+    : limbs (z.get_mpz_t ()->_mp_d),
+      allocated (z.get_mpz_t ()->_mp_alloc), size (z.get_mpz_t ()->_mp_size)
+  {
+    /* Form the expected value independently of set_value and the source.  */
+    if (i % 4 == 1 || i % 4 == 2)
+      {
+        mpz_ui_pow_ui (value.get_mpz_t (), 2, 65537 + i);
+        mpz_add_ui (value.get_mpz_t (), value.get_mpz_t (), i + 23);
+        if (i % 4 == 2)
+          mpz_neg (value.get_mpz_t (), value.get_mpz_t ());
+      }
+  }
+};
+
+static void
+check_value (const mpz_class &z, const saved_value &saved)
+{
+  mpz_srcptr p = z.get_mpz_t ();
+  ASSERT_ALWAYS (mpz_cmp (p, saved.value.get_mpz_t ()) == 0);
+  ASSERT_ALWAYS (p->_mp_d == saved.limbs);
+  ASSERT_ALWAYS (p->_mp_alloc == saved.allocated);
+  ASSERT_ALWAYS (p->_mp_size == saved.size);
+}
+
+static void
+prepare (mpz_class *p, size_t count, std::vector<saved_value> &saved)
+{
+  saved.reserve (count);
+  for (size_t i = 0; i != count; ++i)
+    {
+      ::new (static_cast<void *> (p + i)) mpz_class;
+      set_value (p[i], i);
+      saved.emplace_back (p[i], i);
+    }
+}
+
+static void
+check_values (mpz_class *p, const std::vector<saved_value> &saved)
+{
+  for (size_t i = 0; i != saved.size (); ++i)
+    check_value (p[i], saved[i]);
+}
+
+static void
+clear_values (mpz_class *p, const std::vector<saved_value> &saved)
+{
+  size_t owned = 0;
+  reset_counts ();
+  for (size_t i = 0; i != saved.size (); ++i)
+    {
+      owned += saved[i].allocated != 0;
+      p[i].~mpz_class ();
+    }
+  ASSERT_ALWAYS (allocations == 0 && reallocations == 0);
+  ASSERT_ALWAYS (releases == owned);
+}
+
+/* Reconstruct consumed slots directly, without calling the old destructors.
+   Nonzero replacements demonstrate that those slots can be used normally.  */
+static void
+reconstruct (mpz_class *p, size_t count)
+{
+  for (size_t i = 0; i != count; ++i)
+    {
+      ::new (static_cast<void *> (p + i)) mpz_class;
+      mpz_set_ui (p[i].get_mpz_t (), 7 + i);
+    }
+}
+
+static void
+clear_reconstructed (mpz_class *p, size_t count)
+{
+  for (size_t i = 0; i != count; ++i)
+    {
+      ASSERT_ALWAYS (mpz_cmp_ui (p[i].get_mpz_t (), 7 + i) == 0);
+      p[i].~mpz_class ();
+    }
+}
+
+/* A destination may be raw storage, a destroyed object, or a consumed slot.
+   An allocated guard also detects accidental writes outside the destination.  */
+static void
+check_scalar (size_t kind, unsigned destination_kind)
+{
+  std::allocator<mpz_class> allocator;
+  mpz_class *p = allocator.allocate (4);
+  ::new (static_cast<void *> (p)) mpz_class;
+  set_value (p[0], kind);
+  saved_value saved (p[0], kind);
+  ::new (static_cast<void *> (p + 3)) mpz_class (947);
+  if (destination_kind != 0)
+    {
+      ::new (static_cast<void *> (p + 1)) mpz_class (991);
+      if (destination_kind == 1)
+        p[1].~mpz_class ();
+      else
+        mpz_class::relocate_into (p + 2, p + 1);
+    }
+
+  reset_counts ();
+  ASSERT_ALWAYS (mpz_class::relocate_into (p + 1, p) == p + 1);
+  check_no_memory_operations ();
+  check_value (p[1], saved);
+  reconstruct (p, 1);
+  check_value (p[1], saved);
+  clear_reconstructed (p, 1);
+
+  reset_counts ();
+  p[1].~mpz_class ();
+  ASSERT_ALWAYS (releases == size_t (saved.allocated != 0));
+  ASSERT_ALWAYS (allocations == 0 && reallocations == 0);
+  if (destination_kind == 2)
+    {
+      ASSERT_ALWAYS (p[2] == 991);
+      p[2].~mpz_class ();
+    }
+  ASSERT_ALWAYS (p[3] == 947);
+  p[3].~mpz_class ();
+  allocator.deallocate (p, 4);
+}
+
+/* Exercise destination-only storage whose previous owner was destroyed or
+   consumed, in addition to storage that has never held an mpz_class.  */
+static void
+prepare_destination (mpz_class *p, size_t count, unsigned kind)
+{
+  if (kind == 0 || count == 0)
+    return;
+  std::allocator<mpz_class> allocator;
+  mpz_class *temporary = allocator.allocate (1);
+  for (size_t i = 0; i != count; ++i)
+    {
+      ::new (static_cast<void *> (p + i)) mpz_class (131);
+      if (kind == 1)
+        p[i].~mpz_class ();
+      else
+        {
+          mpz_class::relocate_into (temporary, p + i);
+          temporary->~mpz_class ();
+        }
+    }
+  allocator.deallocate (temporary, 1);
+}
+
+static void
+check_range (size_t count, bool reuse_source, unsigned destination_kind)
+{
+  std::allocator<mpz_class> allocator;
+  const size_t capacity = count + 4;
+  mpz_class *a = allocator.allocate (capacity);
+  mpz_class *b = allocator.allocate (capacity);
+  ::new (static_cast<void *> (a)) mpz_class (101);
+  ::new (static_cast<void *> (a + capacity - 1)) mpz_class (103);
+  ::new (static_cast<void *> (b)) mpz_class (107);
+  ::new (static_cast<void *> (b + capacity - 1)) mpz_class (109);
+  std::vector<saved_value> saved;
+  prepare (a + 1, count, saved);
+  prepare_destination (b + 2, count, destination_kind);
+  reset_counts ();
+  mpz_class::relocate_range (b + 2, a + 1, count);
+  check_no_memory_operations ();
+  check_values (b + 2, saved);
+  if (reuse_source)
+    {
+      reconstruct (a + 1, count);
+      check_values (b + 2, saved);
+      clear_reconstructed (a + 1, count);
+    }
+  clear_values (b + 2, saved);
+  ASSERT_ALWAYS (a[0] == 101 && a[capacity - 1] == 103);
+  ASSERT_ALWAYS (b[0] == 107 && b[capacity - 1] == 109);
+  a[0].~mpz_class ();
+  a[capacity - 1].~mpz_class ();
+  b[0].~mpz_class ();
+  b[capacity - 1].~mpz_class ();
+  allocator.deallocate (a, capacity);
+  allocator.deallocate (b, capacity);
+}
+
+static void
+check_shift (size_t count, size_t shift, bool right,
+             unsigned destination_kind)
+{
+  std::allocator<mpz_class> allocator;
+  const size_t capacity = count + shift + 4;
+  mpz_class *p = allocator.allocate (capacity);
+  mpz_class *from = p + 2 + (right ? 0 : shift);
+  mpz_class *to = p + 2 + (right ? shift : 0);
+  ::new (static_cast<void *> (p)) mpz_class (113);
+  ::new (static_cast<void *> (p + capacity - 1)) mpz_class (127);
+  std::vector<saved_value> saved;
+  prepare (from, count, saved);
+  const size_t consumed_count = std::min (shift, count);
+  mpz_class *destination_only = right && shift < count ? from + count : to;
+  prepare_destination (destination_only, consumed_count, destination_kind);
+  reset_counts ();
+  if (right)
+    mpz_class::relocate_shift_right (from, count, shift);
+  else
+    mpz_class::relocate_shift_left (from, count, shift);
+  check_no_memory_operations ();
+  check_values (to, saved);
+  mpz_class *consumed = right ? from : from + count - consumed_count;
+  reconstruct (consumed, consumed_count);
+  check_values (to, saved);
+  clear_reconstructed (consumed, consumed_count);
+  clear_values (to, saved);
+  ASSERT_ALWAYS (p[0] == 113 && p[capacity - 1] == 127);
+  p[0].~mpz_class ();
+  p[capacity - 1].~mpz_class ();
+  allocator.deallocate (p, capacity);
+}
+
+static void
+check_ordinary_operations ()
+{
+  mpz_class source;
+  set_value (source, 1);
+  saved_value saved (source, 1);
+  mpz_class copy (source);
+  ASSERT_ALWAYS (copy == saved.value);
+  ASSERT_ALWAYS (copy.get_mpz_t ()->_mp_d != saved.limbs);
+  check_value (source, saved);
+  mpz_class moved (std::move (source));
+  check_value (moved, saved);
+  ASSERT_ALWAYS (source == 0);
+  source = 41;
+  ASSERT_ALWAYS (source == 41);
+  check_value (moved, saved);
+  mpz_class assigned (43);
+  assigned = std::move (moved);
+  check_value (assigned, saved);
+  ASSERT_ALWAYS (moved == 43);
+  moved = 47;
+  ASSERT_ALWAYS (moved == 47);
+  copy += 1;
+  ASSERT_ALWAYS (copy != assigned);
+}
+
+int
+main (void)
+{
+  tests_start ();
+  mp_set_memory_functions (count_allocate, count_reallocate, count_free);
+
+  // Ensure the selected libgmpxx was built with this class definition.
+  __gmpxx_mpz_relocation_v2 ();
+
+  static_assert (noexcept (mpz_class::relocate_into (0, 0)), "relocate_into");
+  static_assert (noexcept (mpz_class::relocate_range (0, 0, 0)), "relocate_range");
+  static_assert (noexcept (mpz_class::relocate_shift_right (0, 0, 0)),
+                 "relocate_shift_right");
+  static_assert (noexcept (mpz_class::relocate_shift_left (0, 0, 0)),
+                 "relocate_shift_left");
+
+  // No-op calls still require valid nonnull storage and pointer arithmetic.
+  {
+    mpz_class values[40];
+    std::vector<saved_value> saved;
+    saved.reserve (40);
+    for (size_t i = 0; i != 40; ++i)
+      {
+        set_value (values[i], i);
+        saved.emplace_back (values[i], i);
+      }
+    reset_counts ();
+    mpz_class::relocate_range (values, values + 17, 0);
+    mpz_class::relocate_shift_right (values + 17, 0, 17);
+    mpz_class::relocate_shift_left (values + 17, 0, 17);
+    mpz_class::relocate_shift_right (values + 17, 0, 0);
+    mpz_class::relocate_shift_left (values + 17, 0, 0);
+    mpz_class::relocate_shift_right (values + 17, 17, 0);
+    mpz_class::relocate_shift_left (values + 17, 17, 0);
+    check_no_memory_operations ();
+    check_values (values, saved);
+  }
+
+  for (size_t kind = 0; kind != 4; ++kind)
+    for (unsigned destination_kind = 0; destination_kind != 3;
+         ++destination_kind)
+      check_scalar (kind, destination_kind);
+
+  const size_t counts[] = { 0, 1, 2, 17 };
+  for (size_t i = 0; i != sizeof (counts) / sizeof (counts[0]); ++i)
+    {
+      const size_t count = counts[i];
+      for (unsigned kind = 0; kind != 3; ++kind)
+        {
+          check_range (count, false, kind);
+          check_range (count, true, kind);
+          const size_t shifts[] = { 0, 1, count / 2, count, count + 3 };
+          for (size_t j = 0; j != sizeof (shifts) / sizeof (shifts[0]); ++j)
+            {
+              check_shift (count, shifts[j], false, kind);
+              check_shift (count, shifts[j], true, kind);
+            }
+        }
+    }
+  check_ordinary_operations ();
+
+  tests_end ();
+  return 0;
+}
+
+#else
+int main () { return 0; }
+#endif
--- a/tests/cxx/Makefile.in
+++ b/tests/cxx/Makefile.in
@@ -114,7 +114,8 @@
 @WANT_CXX_TRUE@	t-ternary$(EXEEXT) t-unary$(EXEEXT) \
 @WANT_CXX_TRUE@	t-do-exceptions-work-at-all-with-this-compiler$(EXEEXT) \
 @WANT_CXX_TRUE@	t-ops2z$(EXEEXT) t-assign$(EXEEXT) \
- at WANT_CXX_TRUE@	t-constr$(EXEEXT) t-rand$(EXEEXT)
+ at WANT_CXX_TRUE@	t-constr$(EXEEXT) t-rand$(EXEEXT) \
+ at WANT_CXX_TRUE@	t-relocate$(EXEEXT)
 subdir = tests/cxx
 ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
 am__aclocal_m4_deps = $(top_srcdir)/acinclude.m4 \
@@ -232,6 +233,11 @@
 t_rand_OBJECTS = $(am_t_rand_OBJECTS)
 t_rand_LDADD = $(LDADD)
 t_rand_DEPENDENCIES = $(top_builddir)/tests/libtests.la \
+	$(top_builddir)/libgmpxx.la $(top_builddir)/libgmp.la
+am_t_relocate_OBJECTS = t-relocate.$(OBJEXT)
+t_relocate_OBJECTS = $(am_t_relocate_OBJECTS)
+t_relocate_LDADD = $(LDADD)
+t_relocate_DEPENDENCIES = $(top_builddir)/tests/libtests.la \
 	$(top_builddir)/libgmpxx.la $(top_builddir)/libgmp.la
 am_t_ternary_OBJECTS = t-ternary.$(OBJEXT)
 t_ternary_OBJECTS = $(am_t_ternary_OBJECTS)
@@ -302,7 +308,7 @@
 	$(t_mix_SOURCES) $(t_ops_SOURCES) $(t_ops2f_SOURCES) \
 	$(t_ops2qf_SOURCES) $(t_ops2z_SOURCES) $(t_ops3_SOURCES) \
 	$(t_ostream_SOURCES) $(t_prec_SOURCES) $(t_rand_SOURCES) \
-	$(t_ternary_SOURCES) $(t_unary_SOURCES)
+	$(t_relocate_SOURCES) $(t_ternary_SOURCES) $(t_unary_SOURCES)
 DIST_SOURCES = $(t_assign_SOURCES) $(t_binary_SOURCES) \
 	$(t_cast_SOURCES) $(t_constr_SOURCES) $(t_cxx11_SOURCES) \
 	$(t_do_exceptions_work_at_all_with_this_compiler_SOURCES) \
@@ -311,7 +317,7 @@
 	$(t_mix_SOURCES) $(t_ops_SOURCES) $(t_ops2f_SOURCES) \
 	$(t_ops2qf_SOURCES) $(t_ops2z_SOURCES) $(t_ops3_SOURCES) \
 	$(t_ostream_SOURCES) $(t_prec_SOURCES) $(t_rand_SOURCES) \
-	$(t_ternary_SOURCES) $(t_unary_SOURCES)
+	$(t_relocate_SOURCES) $(t_ternary_SOURCES) $(t_unary_SOURCES)
 am__can_run_installinfo = \
   case $$AM_UPDATE_INFO_DIR in \
     n|no|NO) false;; \
@@ -739,6 +745,7 @@
 t_ostream_SOURCES = t-ostream.cc
 t_prec_SOURCES = t-prec.cc
 t_rand_SOURCES = t-rand.cc
+t_relocate_SOURCES = t-relocate.cc
 t_ternary_SOURCES = t-ternary.cc
 t_unary_SOURCES = t-unary.cc
 t_do_exceptions_work_at_all_with_this_compiler_SOURCES = \
@@ -879,6 +886,10 @@
 t-rand$(EXEEXT): $(t_rand_OBJECTS) $(t_rand_DEPENDENCIES) $(EXTRA_t_rand_DEPENDENCIES) 
 	@rm -f t-rand$(EXEEXT)
 	$(AM_V_CXXLD)$(CXXLINK) $(t_rand_OBJECTS) $(t_rand_LDADD) $(LIBS)
+
+t-relocate$(EXEEXT): $(t_relocate_OBJECTS) $(t_relocate_DEPENDENCIES) $(EXTRA_t_relocate_DEPENDENCIES) 
+	@rm -f t-relocate$(EXEEXT)
+	$(AM_V_CXXLD)$(CXXLINK) $(t_relocate_OBJECTS) $(t_relocate_LDADD) $(LIBS)
 
 t-ternary$(EXEEXT): $(t_ternary_OBJECTS) $(t_ternary_DEPENDENCIES) $(EXTRA_t_ternary_DEPENDENCIES) 
 	@rm -f t-ternary$(EXEEXT)
@@ -1265,6 +1276,13 @@
 	--log-file $$b.log --trs-file $$b.trs \
 	$(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
 	"$$tst" $(AM_TESTS_FD_REDIRECT)
+t-relocate.log: t-relocate$(EXEEXT)
+	@p='t-relocate$(EXEEXT)'; \
+	b='t-relocate'; \
+	$(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+	--log-file $$b.log --trs-file $$b.trs \
+	$(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+	"$$tst" $(AM_TESTS_FD_REDIRECT)
 .test.log:
 	@p='$<'; \
 	$(am__set_b); \


More information about the gmp-devel mailing list