CSCI 485 Lab 3: C++ template programming for compile-time computation

Addenda to constexpr evaluation

It turns out the compiler can, in the interests of "optimization", actually choose to evaluate constexpr's at run time (live and learn)!

In those case, it appears to be setting aside the storage for the "constant", calculating it at run time, but treating it as read only after the value is initially set.

In that case it appears the only way to be certain whether it is performing the computation at compile time is to break out the compile-to-assembly option - which I won't require folks to do.

Since C++14, things have changed further, allowing the use of non-constexpr variables within constexpr functions. It appears that going back to compiling with -std=c++11 gives stricter controls over this, but limits the body of a constexpr function to being strictly a return statement (though nested :? operators can still be embedded within that).

The objective for this lab is to gain an understanding of the use of C++ templates to carry out computations at compile time.

As we have seen in the lectures, we can perform significant computation and code manipulation in this manner:

In assignment 3, you'll be implementing a set of functions capable of operating on constant structs. In this case, the structs are representing Fractions, e.g:

struct Fraction { int num, den; };

const Fraction f = { 3, 4 };   //  f represents 3/4
The functions you are implementing will perform basic math operations on fractions and returning fractions as a result, e.g. addition, subtraction, etc.

Since the functions need to work at compile time, the safest approach is to declare the return type for each as a constant expression, e.g. the function square below returns a fraction multiplied by itself:

constexpr Fraction square(const Fraction f) {
   const Fraction result = { f.num*f.num, f.den*f.den };
   return result;
}
To be valid constant expressions, your functions are limited to basic operations on constant values, calls to constexpr functions, and the use of the ternary operator, e.g.

// if f holds a fraction whose numerator is greater than its denominator,
//    return the square of its inverse,
// otherwise return the square of the original
constexpr Fraction squareSmallOverBig(const Fraction f)
{
   // have an inverted version of the fraction ready
   const Fraction inverse = { f.den, f.num };

   // if the numerator is bigger than the denominator return the inverted version,
   // otherwise return the original
   return (f.num > f.den)? square(inverse) : square(f);
}

Sample function implementations for runtime computation are provided in runtime_fractions.h, but you'll need to rework those considerably to make constexpr equivalents.

Suggested order of development

As with any tricky programming, incremental development and debugging is highly recommended.

Initially, I would recommend having Simplify simply return the Fraction it was given, and having gcd return 1.

Implement the other functions (lcm, negate, invert, mult, add, sub, div) and debug until you get the correct basic behaviour, without worrying about simplification or divide-by-zero cases.

Once those are working, get the gcd algorithm working. The recursive algorithm presented works well for the ?: operator. Once it is working correctly I'd recommend adding an error-check in case both a AND b are 0. If both are zero then simply return 1 as the gcd (this will eliminate many of the divide-by-zero floating point exceptions you would otherwise encounter).

Once that is working, perhaps include an extra set of tests in the add(f1,f2) function to catch the cases where either fraction has a zero-valued denominator, again to prevent floating point exceptions.

For the simplify function, keep in mind that you can have local constant fractions, like in the square example above. You can then use these intermediate values and the ternary operator to compute and return the appropriate final answer.