flintxx – C++ Wrapper

Introduction

In this section we describe how to use flintxx the Flint C++ wrapper.

We begin with a simple example:

#include "fmpzxx.h"

using namespace flint;
fmpzxx x, y;
x = 7u;
y = x*x;
std::cout << x << "^2 = " << y << std::endl;

As can be seen, if a FLINT C interface is called foo``and resides in ``foo.h, then the C++ version is called fooxx and resides in fooxx.h. All flintxx classes live inside namespace flint.

Functions which operate on wrapper classes are usually implemented as overloaded stand-alone functions, with the type prefix dropped. For example, a call to flint::gcd(f1, f2) yields an expression template evaluating via fmpz_gcd, provided f1 and f2 evaluate to instances of fmpzxx.

Sometimes we felt that dropping the type prefix would yield incomprehensible names, as for example in fmpq_next_minimal, or fmpq_reconstruct. In these cases the type prefix is swapped for the flintxx equivalent, so the flintxx version would be called fmpqxx_reconstruct.

In this situation, usually the same functionality is also exposed as a (possibly static) member function, and this is the preferred way of accessing the functionality. Thus one should write fmpqxx::reconstruct(a, m) or fmpqxx(0, 1u).next_minimal().

Expression templates

The implementation of flintxx tries very hard not to incur any overhead over using the native C interface. For this reason, we use expression templates for lazily evaluating expressions. This allows us to avoid creating excessively many temporaries, for example.

This means that even if x and y are of type fmpzxx, then x + y will not be of type fmpzxx. Instead it will be an object which for most purposes behaves just like fmpzxx, but really only expresses the fact that it represents the sum of x and y.

This distinction almost never matters, since expression templates are evaluated automatically in most cases. Thus cout << x + y or x + y == 7 will work just as one might expect.

There are ways to request explicit evaluation of an expression template, most notably (x + y).evaluate() and fmpzxx(x + y).

One caveat of the expression template approach is that compiler error messages can be long and hard to understand.

In flintxx we strive to type-check template parameters as early as possible in order to keep error messages short and close to the actual user error. Excessively long error messages are often indicative of a bug in flintxx.

Tuples

Many FLINT functions naturally return two or more arguments. A typical example is divrem. The underlying C function is void fmpz_poly_divrem(fmpz_poly_t Q, fmpz_poly_t R, const fmpz_poly_t A, const fmpz_poly_t B).

Mapping this directly to C++ would yield something like void divrem(fmpz_polyxx& Q, fmpz_polyxx& R, const fmpz_polyxx& A, const fmpz_polyxx& B)}.

While functional, this is not particularly nice; the syntax divrem(Q, R, A, B), where the first two arguments are modified, is just very reminiscent of C. We prefer an expression closer to the python analogue (Q, R) = divrem(A, B).

For this purpose, flintxx uses lazy tuples and the following is a valid flintxx expression: ltupleref(Q, R) = divrem(A, B).

For the purpose of this documentation, ltuple types are denoted as follows: Ltuple<Type1, Type2, ..., Typer>.

Thus, divrem would return an object of type Ltuple<fmpz_polyxx, fmpz_polyxx>.

The user should never try to construct such types by hand; instead use the function ltupleref (and perhaps occasionally ltuple; both documented later).

One thing to observe is that ltuples are typed fairly weakly. Thus assignments and equality comparisons can be performed as long as both sides have the same length, and the operation can be performed on all components (whether or not the component types match).

Another interesting feature of ltuples is the type flint::detail::IGNORED_TYPE. In an ltuple assignment, where the left hand side contains a reference to this type, the relevant entry is just discarded.

Since the ltuple.h header automatically creates a static instance _ of this type, in the following listing, the lines marked (1) and (2) have the same effect (but the second is potentially more efficient).

#include "fmpz_polyxx.h"

using namespace flint;

fmpz_polyxx f, g;

fmpz_polyxx R;
ltupleref(_, R) = divrem(f, g); // (1)
R = f % g;                      // (2)

Note finally that using ltuple intermediates often results in more copies than necessary. For example the expression ltupleref(num, _) = divrem(a, b) assigns the quotient to num, creating just a temporary fmpzxx to hold the remainder. In contrast, num = divrem(a, b).get<0>() creates two temporary instances of fmpzxx.

Reference types

One subtlety in wrapping a C library is that references do not work as easily as one might expect. For example, consider the class fmpqxx, wrapping fmpq_t, i.e. rational numbers. As such, an instance of fmpqxx has a numerator and denominator. In C, these are accessible via macros fmpq_numref and fmpq_denref, which yield fmpz*, which can be used essentially interchangeably with fmpz_t. In particular, any library function which operates on fmpz_t can operate on the numerator or denominator of an fmpq_t. In C++, we would like to have a member functions den` and ``num which return an object of type fmpzxx& (i.e. a reference to fmpzxx).

However, this is not possible, since fmpqxx is not implemented as a pair of fmpzxx, and instead simply contains an fmpq_t.

For this reason, for every C interface foo, flintxx provides two additional types, called fooxx_ref and fooxx_srcref, acting as replacements for fooxx& and const foox&, respectively, in situations where no underlying C++ object exists.

Instances of fooxx_ref or fooxx_srcref behave exactly like instances of fooxx. In fact, the user should never notice a difference. Any flintxx operation or expression which works on objects of type foo also works on objects of type fooxx_ref and fooxx_srcref.

Moreover, instances of foo can be converted implicitly to fooxx_ref or fooxx_srcref, and fooxx_ref can be converted implicitly to fooxx_srcref.

It is also possible to explicitly convert reference types fooxx_*ref to fooxx (since this entails copying, we provide no implicit conversion).

In summary, the class fooxx_ref behaves like a reference to an object of type fooxx. As such it can be used both as a right hand side and as a left hand side, just like fooxx.

The class fooxx_srcref behaves like a reference to a constant object of type fooxx, and so cannot be used as a left hand side. These objects are created by flintxx automatically,`for example upon calling fmpqxx::num().

Unified coefficient access

Consider again the x.num() method of fmpqxx. In various situations, this can have different return types. Namely, if x is a writable expression, then x.num() returns an fmpzxx_ref. In particular the return value behaves just like fmpzxx, no evaluation is necessary to obtain it, there are no copies, and it is possible to change the return value (and thus change x).

If on the other hand x is a readonly immediate, then the return value of x.num() has type fmpzxx_srcref. This again behaves just like fmpzxx and no evaluations or copies are necessary, but this time it is not possible to change the return value (and so it is not possible to change x, either).

Finally, if x is a lazy expression, then the return value is actually a lazy expression template. Thus to obtain the “actual” value of x.num(), evaluations are necessary, and potentially so are copies.

Thus in any case the return value behaves just like fmpqxx, but apart from that the behaviour of x.num() varies quite drastically in the different situations. We call this “unified coefficient access” (the coefficients of a fmpqxx being num(), den()), and the same behaviour occurs in many other flintxx types, e.g. in fmpz_polyxx.coeff(), etc.

Type conversion

As a rule, flintxx does not perform automatic type conversions (except when related to the promotion to reference types, c/f earlier discussion). In expression templates, operands can be automatically promoted if the underlying C interface provides this facility. Beyond that, types have to be converted explicitly.

There are two ways of doing this. The preferred one is using static constructor functions. Typical examples are fmpz_polyxx::from_ground(fmpzarg) and nmod_polyxx::reduce(mplimbarg, nmodctxarg). The former takes an (expression template evaluating to) fmpzxx and returns an fmpz_polyxx representing the constant polynomial with value the fmpzxx. The latter takes an argument of type mp_limb_t and one of type nmodxx_ctx_srcref (essentially a word-sized modulus) and returns an nmod_polyxx representing the constant polynomial obtained by reducing mplimbarg.

The general format for this is totype::constructorname(arg1, arg2, ...). We prefer this because it makes explicit the type that is being converted to, and the way the arguments are to be interpreted.

This format only works if the target type is part of flintxx. In other cases, we use a .to<totype>() syntax, as in fmpzexpr.to<slong>().

Input and output

In C++ it is customary to provide input and output via iostreams, and overloading the operators << and >>. When wrapping a C library which works on the FILE interface, this is rather hard to accomplish.

For this reason, flintxx only provides streaming output (i.e. <<), and only when there is a to_string method. Unfortunately this applies to only a small subset of the FLINT types.

For output in other cases, and input in all cases, we provide C-like functions. Namely, the functions print, print_pretty, read and read_pretty can be used similarly to the C flint_printf and scanf.

For example, print(x) where x is an fmpz has the same effect as std::cout << x.

Inheritance and flintxx

The flintxx classes are not designed for inheritance. If you want to modify behaviour, you should wrap flintxx types into your own classes (extension by aggregation, not inheritance).

Notation and conventions in flintxx documentation

As explained above, the flintxx classes and functions perform quite a number of operations which should be invisible to the user. Some template types implement methods which only make sense for some template arguments, etc.

For example, every expression template built from fmpq_polyxx (polynomials with rational coefficients) has a method set_coeff. However, this method only makes sense for objects of type fmpq_polyxx or fmpq_polyxx_ref (calling it on other types will result in a compilation error), and its existence in objects of other types should be considered an implementation detail.

In what follows, we document a “virtual” set of classes and functions, which explain how the user should expect its objects to behave, and which we guarantee to maintain. Other interfaces should be considered implementation details and subject to change.

Consider the interface fmpzxx, and more concretely an instance a. As in the above discussion, we see that from a we can build a lot of different objects: expression templates like a+a, constant objects like const fmpzxx& b = a;, reference objects like fmpzxx_ref c(a), etc. These by nature behave somewhat differently. For our purposes, we classify types into “targets” (things which can be assigned to), “sources” (things which contain actual computed data, or references thereto, as opposed to lazy expression templates) and “expressions” (sources or expression templates).

Note that every target is a source, and every source is an expression.

We denote any type which can act as a target for fmpzxx as Fmpz_target (note the initial capital letter!), any fmpzxx source as Fmpz_source and any fmpzxx expression as Fmpz_expr. Such made up type names (always with initial capital letter) are referred to as “virtual types” in the documentation. These are used for all flint classes (e.g. Fmpq_expr or Fmpz_polyxx_src).

When using virtual types, we will suppress reference notation. No flintxx types are ever copied automatically, unless the documentation explicitly says so. This is a general philosophy of flintxx: the library does as many things automatically as it can, without introducing additional calls to underlying Flint C functions. So for example, it is not possible to implicitly convert int to fmpzxx (since doing so requires a C call). Of course explicit conversions (or assignments) work completely fine.

It is also often the case that flintxx functions are conditionally enabled templates. A notation such as void foo(T:is_signed_integer) denotes a template function which is enabled whenever the template parameter T satisfies the type trait is_signed_integer. These type traits should be self-explanatory.

In what follows, we will never document copy constructors, or implicit conversion constructors pertaining to reference types. We will also not document assignment operators for expressions of the same type. Thus if x is an fmpzxx and y is an fmpqxx, then x = x and y = x are both valid, but only the second assignment operator is documented explicitly.

Most flintxx functions and methods wrap underlying C functions in a way which is evident from the signature of the flintxx function/method. If this is the case, no further documentation is provided. For example, the function double dlog(Fmpz_expr x) simply wraps double fmpz_dlog(const fmpz_t).

As is evident from the return type, dlog immediately evaluates its argument, and then computes the logarithm. In contrast, a function like Fmpz_expr gcd(Fmpz_expr, Fmpz_expr) returns a lazily evaluated expression template and wraps void fmpz_gcd(fmpz_t, const fmpz_t, const fmpz_t).

In case a Flint C function has more than one return value in the form of arguments passed in by reference, the C++ wrapper returns an ltuple. In this case, the order of the ltuple arguments is the same as the order of the function arguments; so for example ltupleref(Q, R) = divrem(A, B) has the same effect as fmpz_poly_divrem(q, r, a, b), provided Q, R, A, B are fmpz_polyxx and q, r, a, b are the underlying fmpz_poly_t.

If such a convention is followed, the documentation below may not further explain anything. In all other cases, further explanation is provided (this applies in particular if the C function has return type different from void).

Global functions or member functions?

Often it is not clear if functionality is exposed as a global function, such as gcd(a, b), or as a member function, such as a.gcd(b). In flintxx, we strive to make both available when feasible.

In the documentation, the global versions are documented in detail (explaining the allowed types etc), whereas the member function versions are summarised more briefly under e.g. Fmpz_expr::unary operation() const, Fmpz_expr::binary operation(??) const etc.