Integer tutorial

integer was the first class implemented in mp++, and as a result it is currently the most featureful and optimised among mp++’s multiprecision classes.

One of the first things that can be noticed about integer is that the class is parametrised over an integral value SSize, called the static size. This compile-time constant represents the number of limbs that will be stored directly within an integer object before resorting to dynamic memory allocation. A limb is the part of a multiprecision integer that fits in a single word-sized unsigned integral type: in the same way an integral value in base 10 is represented as a sequence of digits in the \(\left[0,9\right]\) range, an integer object is represented internally by an array of 32-bit or 64-bit unsigned C++ integral values. Thus, for instance, if SSize is set to 2 on a 64-bit system, integer is able to represent values in the \(\left(-2^{128},2^{128}\right)\) range without resorting to dynamic memory allocation. In general, an SSize value of 1 is a good default choice for many use cases.

In addition to the common operators available for all of mp++’s multiprecision classes, integer supports the following additional operators:

  • the modulo operator %, which computes the remainder of integer division,

  • the bitshifting operators << and >>,

  • the bitwise logical operators ~, &, | and ^.

In addition to the binary versions of these operators, the in-place versions are also available. Lower level ternary primitives (e.g., mul_2exp(), tdiv_q_2exp(), bitwise_ior(), etc.) are also provided for those situations in which it is desirable to pass the return value as a function parameter, rather than creating a new return value (as explained earlier in the API overview). For consistency with C++11, the % operator returns a remainder with the same sign as the dividend. The bit-shifting operators << and >> correspond respectively to multiplication and division by a power of 2. The bitwise logical operators behave as-if integer used a two’s complement representation (even if internally a sign-magnitude representation is used instead).

Among the features specific to integer we find:

Many of these features, which are documented in detail in the integer reference, are available in multiple overloads, often both as free and member functions.

Interacting with the GMP API

integer provides a variety of ways for interfacing with the GMP library. There are a few reasons why one would want to use integer in conjunction with the GMP API, such as:

  • the necessity of using functions from the GMP API which have not (yet) been wrapped/implemented by mp++,

  • passing data from/to mp++ to/from another GMP-based multiprecision library.

To start with, integer is constructible and assignable from mpz_t objects:

mpz_t m;
mpz_init_set_si(m, -4);  // Init an mpz_t with the value -4.

int_t n1{m};             // Init an int_t from the mpz_t.
assert(n1 == -4);        // Verify that the value is correct.

int_t n2;
n2 = m;                  // Assign the mpz_t to another int_t.
assert(n2 == -4);        // Verify that the value is correct.

mpz_clear(m);            // Clear the mpz_t.

Second, it is possible to get a reference to an mpz_t from an integer via the get_mpz_t() member function. This member function will first switch the calling integer to dynamic storage (if the calling integer is not already employing dynamic storage), and it will then return a raw non-owning pointer which can be used both as a const and mutable parameter in the GMP API. For example:

mpz_t b;
mpz_init_set_si(b, -4);                   // Init an mpz_t with the value -4.

int_t a, c{2};                            // Init two integers.

mpz_add(a.get_mpz_t(), b, c.get_mpz_t()); // Compute b + c via the GMP API, storing the result in a.

assert(a == -2);                          // Verify that the result is correct.

mpz_clear(b);                             // Clear the mpz_t.

It is important to emphasise that get_mpz_t() forces the use of dynamic storage, thus incurring in a potential performance hit. If only const access is needed, a better alternative to get_mpz_t() is the get_mpz_view() member function. get_mpz_view() returns a read-only view of the calling integer which is implicitly convertible to a const mpz_t, and which can thus be used as a non-mutable function parameter in the GMP API. The creation of the read-only view is lightweight, and, crucially, it does not force the use of dynamic storage in the calling integer. We can slightly modify to previous example to use a read-only view as the third parameter in the mpz_add() call, and verify that the creation of the read-only view did not trigger a promotion from static to dynamic storage:

mpz_t b;
mpz_init_set_si(b, -4);                      // Init an mpz_t with the value -4.

int_t a, c{2};                               // Init two integers.

mpz_add(a.get_mpz_t(), b, c.get_mpz_view()); // Compute b + c via the GMP API, storing the result in a.

assert(a == -2);                             // Verify that the result is correct.
assert(c.is_static());                       // Verify that c is still using static storage.

mpz_clear(b);                                // Clear the mpz_t.

It must be noted that both get_mpz_t() and get_mpz_view() have to be used carefully, as they return non-owning objects which can easily lead to dangling pointers or references, if misused. The documentation of the two functions explains in detail some of the potential pitfalls that users need to be aware of.

Serialisation

New in version 0.7.

mp++ provides a simple API for the (de)serialisation of integer objects into/from memory buffers and C++ streams. Possible uses of the serialisation API include persistence (e.g., saving/loading integer values to/from a file), the transmission of integer objects over the network (e.g., in distributed computing applications), inter-process communication, etc. The API consists of two main overloaded functions, mppp::integer::binary_save() and mppp::integer::binary_load() (plus their free-function counterparts).

Let’s see a few examples of the serialisation API in action:

int_t a{42}, b;
char buffer[1024];     // Provide ample storage for serialisation.

a.binary_save(buffer); // Serialise a into the buffer.

b.binary_load(buffer); // Deserialise the content of the buffer into b.

assert(b == a);        // Check that the original value is recovered.

Here we are serialising the value a into a char buffer of size 1024. In this case we know that, since the original value is small, a buffer of 1024 bytes will provide more than enough space to store the serialised representation of a. The content of the buffer is then read back into the b variable, and we verify that b’s value is indeed equal to the value of a.

Additional overloads are available for the save/load functions, targeting std::vector<char>, std::array<char> and C++ streams. The std::vector<char> binary_save() overload takes care of enlarging the target vector as much as needed to store the serialised integer:

int_t a{42}, b;
std::vector<char> buffer;   // An initially-empty vector buffer.

a.binary_save(buffer);      // Serialise a into the buffer.
assert(buffer.size() != 0); // Check that the buffer was resized.

b.binary_load(buffer);      // Deserialise the content of the buffer into b.

assert(b == a);             // Check that the original value is recovered.

Both mppp::integer::binary_save() and mppp::integer::binary_load() retun a std::size_t value representing the number of bytes written/read during (de)serialisation, or zero if an error occurred. A return value of zero can occur, for instance, when serialising into a std::array<char> with insufficient storage:

int_t a{42}, b;
std::array<char, 1> buffer_small;       // A std::array<char> with insufficient storage.

auto ret = a.binary_save(buffer_small); // Try to serialise a into the small buffer.
assert(ret == 0);                       // Verify that an error occurred.

std::array<char, 1024> buffer_large;    // A std::array<char> with ample storage.

ret = a.binary_save(buffer_large);      // Serialise a into the large buffer.
assert(ret != 0);                       // Check that no error occurred.

b.binary_load(buffer_large);            // Deserialise the content of the buffer into b.

assert(b == a);                         // Check that the original value is recovered.

The amount of bytes of storage necessary to serialise an integer can always be computed via the mppp::integer::binary_size() function.

Overloads for C++ streams work in a (hopefully) unsurprising fashion:

int_t a{42}, b;
std::stringstream ss; // Let's use a string stream for this example.

a.binary_save(ss);    // Serialise a into the stream.

b.binary_load(ss);    // Deserialise the content of the stream into b.

assert(b == a);       // Check that the original value is recovered.

A couple of important points about the serialisation API need to be emphasised:

  • although mp++ does run some consistency checks during deserialisation, the API is not built to protect against maliciously-crafted data. Users are thus advised not to load data from untrusted sources;

  • the current binary serialisation format is compiler, platform and architecture specific, it is not portable and it might be subject to changes in future versions of mp++. Users are thus advised not to use the binary serialisation format for long-term persistence or as a data exchange format: for such purposes, it is better to use the string representation of integer objects.