API overview

In addition to the common arithmetical and relational operators introduced earlier, mp++’s API provides a rich set of functions. Generally speaking, the mp++ API operates on two levels:

  • a low-level API closely following the APIs of the multiprecision libraries on top of which mp++ is built,

  • a higher-level API more focused on consistency and user-friendliness.

The intent is to provide users with the flexibility of switching between low-level primitives (e.g., when performance is critical) and higher-level, more readable and user-friendly code.

To give an idea of the type of tradeoffs involved in the choice between low-level and high-level API, consider the simple task of adding two multiprecision integers a and b. This can be accomplished either via the overloaded binary operator for integer (as seen in the previous section), or via the lower-level add() primitive (which is documented in the integer reference):

int_t a{2}, b{4};

auto c1 = a + b; // Binary operator.

int_t c2;
add(c2, a, b);   // Low-level add() primitive.

assert(c1 == c2);

Similarly to the mpz_add() function from the GMP API, mp++’s add() is a ternary function taking as first parameter the return value (by reference), and as second and third parameters the operands. Although the end result is identical, there is a fundamental difference in terms of the number of operations performed by the two approaches: the binary operator returns a new value, which is then used to move-initialise c1 1, while the add() function writes the result of the addition directly into an existing value. While, in practice, the difference might not matter in the simple example above, it will start to matter in less trivial cases:

std::vector<int_t> v = {...};

int_t sum1;
for (const auto &n: v) {
  sum1 = sum1 + n;    // Binary operator.
}

int_t sum2;
for (const auto &n: v) {
  add(sum2, sum2, n); // Low-level add() function.
}

assert(sum1 == sum2);

In this example, we are computing the sum of the integral values held in a vector v. When using the binary operator, at each step of the iteration a new temporary integer is constructed by the expression sum1 + n, and this temporary value is then move-assigned to sum1. When using the add() function, the creation of the temporary is avoided altogether as the result of the addition is written directly into the accumulator sum2. Thus, the first version of the loop will necessarily be less efficient than the second one. Note that, in this specific case, we can recover optimal performance while maintaining a nice syntax by replacing the binary operator with an in-place operator (which will avoid the creation of unnecessary temporaries):

int_t sum1;
for (const auto &n: v) {
  sum1 += n; // In-place addition operator.
}

Let’s see another example:

std::vector<int_t> v = {...};

int_t gcd1;
for (const auto &n: v) {
  gcd1 = gcd(gcd1, n); // Binary gcd() function.
}

int_t gcd2;
for (const auto &n: v) {
  gcd(gcd2, gcd2, n);  // Ternary gcd() function.
}

assert(gcd1 == gcd2);

Here we are computing the GCD of the integers stored in the vector v. mp++ provides two overloads for the gcd() function:

  • a binary overload, taking as input the two operands, and returning their GCD,

  • a ternary overload, taking the return value as first parameter and the two operands as second and third parameters.

Like in the previous example, the ternary overload avoids the creation and subsequent assignment of a temporary value, and will thus perform better. The binary GCD overload, on the other hand, is easier to use (no need to prepare a return value beforehand) and closer to a functional style. The presence of binary and ternary overloads for the same functionality is not restricted to gcd(), but it’s a common feature for many of mp++’s binary functions and operators.

For unary functions and operators, there’s an additional degree of freedom in the API. Unary functions in mp++ are often provided with the following set of overloads:

  • an in-place nullary member function,

  • a functional-style unary function,

  • a GMP-style binary function.

As a concrete example, let’s take a look at different ways of computing the absolute value of an integer:

int_t n1{-5};
n1.abs();              // In-place nullary member function.
assert(n1 == 5);

int_t n2{-5};
auto n2_abs = abs(n2); // Unary function.
assert(n2_abs == 5);

int_t n3{-5}, n3_abs;
abs(n3_abs, n3);       // GMP-style binary function.
assert(n3_abs == 5);

The mppp::integer::abs() member function computes and stores the absolute value directly into the calling object. The unary function (much like std::abs()) takes as input an integer and returns its absolute value. The GMP-style binary abs() function stores into the first argument the absolute value of the second argument.

The nullary member function overload is provided to cater to the common use case in which a value is mutated in-place through a unary operation (e.g., n = abs(n)). Since the nullary member function overloads return a reference to the calling object, they can be chained to perform a sequence of in-place operations on the same value:

int_t n1{-16};
n1.abs().sqrt().neg(); // Equivalent to: n1 = neg(sqrt(abs(n1)))
assert(n1 == -4);

Footnotes

1

Of course, copy elision in this specific case will most likely eliminate any move operation. But, for the sake of argument, let’s pretend that it will not :)