CPP02 · handbook
⏯ tracer 🧮 lab 🗺 map
42 common core · c++ module 02 · defense handbook

CPP02 / the Fixed class, explained

Four exercises, one class, and a handful of ideas that look like one mysterious mechanism until they are pulled apart. This handbook pulls them apart. It starts with the object model on a class that has nothing to do with numbers, then adds the number, then walks every exercise and every trace line. For the moving picture, open the tracer ⏯ and watch objects appear and die as the subject's main runs.

00 · orientationOne class, four layers

Module 01 gave you seven independent exercises. Module 02 gives you one class, Fixed, built in four layers. A decision made in ex00 is still alive in ex03, and that is both the difficulty and the lesson: you are watching a type grow.

What each layer teaches

ExerciseWhat is addedThe idea being taughtThe trap
ex00Orthodox Canonical Form and two raw accessorsconstruction, copying, assignment and destruction are four distinct events, and the trace proves which one ranthe copy constructor and the assignment operator are not the same thing
ex01int and float constructors, toFloat, toInt, stream insertiona class can pretend to be a number, and overload resolution picks the constructoran integer division hiding inside toFloat, and a bare 10.0 literal that is ambiguous
ex02sixteen operators and four static functionsoperators are ordinary member functions with special names, and constness alone can select an overloadmultiplication on raw values overflows at 182
ex03Point with Fixed const members, bspcomposition, const members, and a signature that copies by valueconst members make the required assignment operator a no op

The rules that apply everywhere

  • c++ -Wall -Wextra -Werror -std=c++98, zero warnings. Everything here is also clean under -pedantic.
  • Orthodox Canonical Form is mandatory from this module to module 09. Every class writes out its default constructor, copy constructor, copy assignment operator and destructor, even when the compiler generated ones would behave identically.
  • No friend, no using namespace, no STL containers, no C++11. roundf from <cmath> is authorized from ex01.
  • Function bodies never live in a header. Headers carry include guards and their own dependencies.
  • No comments in any source file. Explanations live in the walkthrough and here.

File map

ex00/ 4 files · ./fixed
Fixed.hppthe class definition, no includes at all, the canonical four plus getRawBits and setRawBits
Fixed.cppthe six member bodies, each special member prints its line, operator= reads through the getter
main.cppthe subject's program verbatim, because the subject also supplies the expected output
Makefilesame shape as every module, NAME = fixed
ex01/ 4 files · ./fixed
Fixed.hppgains <ostream>, the two converting constructors, toFloat, toInt, and the free operator<< declared after the class
Fixed.cppgains <cmath> for roundf; operator= now reads other._value directly
main.cppsubject's program verbatim
ex02/ 4 files · ./fixed
Fixed.hppadds the sixteen operators and the four static min / max declarations
Fixed.cppcomparisons on raw values, + and - on raw values, * and / through float, prefix and postfix increments, min and max
main.cppthe subject's snippet plus four labelled test sections, because this subject says to test every feature
ex03/ 7 files · ./bsp
Fixed.hpp / .cppbyte identical to ex02
Point.hpp / .cpptwo Fixed const members, canonical four, float constructor, two getters returning const references, and the bsp prototype
bsp.cppa file local cross helper and bsp itself
main.cppa self checking harness of nineteen cases with a summary line and a nonzero exit on failure

01 · orientationWhat the compiler sees

Before any object exists there is a build, and half the confusion about “which file does what” dissolves once the build is a picture. Three source files become one program in two steps.

The header is a contract, not a program

Fixed.hpp says what exists. A class named Fixed, holding an int, offering these functions with these signatures. It contains no bodies, so nothing in it ever runs. #include is literal text pasting, so both Fixed.cpp and main.cpp end up with an identical copy of that contract at their top. That is why a header must have an include guard. If two headers both pulled in Fixed.hpp, the class would be defined twice in one file, and the guard turns the second copy into nothing.

#ifndef FIXED_HPP
# define FIXED_HPP
class Fixed { ... };
#endif

The .cpp is the implementation

Fixed.cpp says how. Every body starts with Fixed::, which reads as “this belongs to Fixed”. It includes the header so the compiler can check each body against its declaration. Nothing in Fixed.cpp runs top to bottom either. It is a catalogue of operations waiting to be called.

main.cpp is the only thing that runs

Execution starts in main and goes nowhere else unless main calls something. When it writes Fixed a; the compiler consults the contract, finds a default constructor, and the linker later finds its body in Fixed.o. That is the whole relationship. Header declares. Source defines. Main requests.

a link error that looks like a compiler error static const int _fractionalBits = 8; has a value but no storage. That is fine as long as you only read it. Bind it to a reference or take its address and the linker reports undefined reference to Fixed::_fractionalBits, late, pointing at an object file rather than a line of source. Measured. This module never needs its address, so the rule is simply never to pass it to anything taking a reference.

Where the operator<< lives, and why

One declaration sits outside the class, at the bottom of Fixed.hpp. It is still part of the contract, it just is not a member. Z6 explains why it cannot be one.

02 · orientationThe other three tools

This handbook is the text. Three companions show the same ideas moving.

Suggested order if the module feels like fog. Read Level zero here, then run ex01 in the tracer with ▶ step, then come back for the number.

Z1 · level zeroClass, object, state

Forget Fixed for a moment. Nothing about the object model needs 256 or a triangle, and mixing them together is exactly what made the module hard to hold. Here is a class that counts.

class Counter
{
private:
	int	_value;

public:
	Counter();
	void	addOne();
	int	getValue() const;
};

The class is a description. It says that a Counter has one integer inside and three things you can do to it. Writing the class creates nothing. No memory is set aside, no _value exists yet. It is a blueprint, or better, a catalogue of operations.

An object is one actual instance. Counter a; reserves memory for one int and calls the constructor on it. Counter b; reserves another. Each object has its own _value. That per object data is its state.

Two things follow immediately. The functions are not copied per object. There is one addOne in the program, and it works on whichever object it was called through. And private means the class controls access, not the object. Any member function of Counter may read any Counter's _value, including another object's. That is the fact that lets operator= read other._value in ex01 without any friend.

The same picture for Fixed

Fixed is structurally a Counter. One int of state, called _value. What makes it a number type is a rule the class enforces about that integer, which N1 introduces. The rule is not stored anywhere. The object contains 2560, and the class interprets it as 10.

hold on to this A Fixed does not contain both 10 and 2560. It contains 2560. Every function on it either reads that integer or writes it, and the “number” is a story the functions agree to tell about it.

Z2 · level zeroMember functions and this

How does a.addOne() know to increase a and not b? The compiler sees that a has type Counter, looks in Counter's catalogue for addOne, and calls it with a hidden extra argument. Inside the function that hidden argument is named this, and it is a pointer to the object the call went through.

So _value += 1 inside the body really means this->_value += 1, and *this means “the object itself”. That is why the copy constructor in every exercise can write *this = other: it is saying “assign other into the object currently being constructed”.

A const at the end of a member function, as in int getValue() const, is a promise about this. The function will read the object and never write it. The compiler enforces it, and it is also what allows the function to be called on a const object. Fixed const b(10); b.toInt(); compiles only because toInt made that promise.

A static member function has no this. Fixed::max(a, b) is called through the class, not an object, because comparing two objects is nobody's method in particular. Both arguments arrive explicitly.

Z3 · level zeroBorn versus changed

This is the distinction the whole module turns on, and the one most often blurred. There are two entirely different ways for an object to end up holding the value of another.

a new object is born
Fixed b( a );
Fixed d = b;     // also construction

b did not exist. It is being created using a. The copy constructor runs. The = in the second line is initialization syntax, not assignment, because d is being declared on that line.

an existing object is changed
Fixed c;
c = b;

c already exists, built by the default constructor one line earlier. Now its contents are overwritten. The copy assignment operator runs. No object is created.

The four canonical members are the four moments in an object's life, and each is a separate function you write.

Why the copy constructor in this repo produces three lines

ex00's expected trace shows Copy constructor called, then Copy assignment operator called, then getRawBits member function called. That happens only if the copy constructor is written as a delegation.

Fixed::Fixed(const Fixed &other) : _value(0)
{
	std::cout << "Copy constructor called" << std::endl;
	*this = other;
}

“Construct me at zero, then assign the other into me.” The assignment operator prints its own line and, in ex00, reads through the getter, which prints a third. A copy constructor that wrote : _value(other._value) would produce one line, which is why the subject marks the middle line as one that may be missing. It is not a different mechanism, only a different choice about reuse. The tracer shows both lines landing under the copy constructor in ex00, statement 2.

say it in one sentence Construction brings an object into existence. Assignment changes one that already exists. Fixed b(a) is construction even though it copies, and c = b is assignment even though c ends up identical to b.

Z4 · level zeroWho gets called

Several functions share a name and the compiler picks one. That is overloading, and the picking is overload resolution. It is not magic and it is not guesswork. It is a short procedure run at compile time, and once you can run it in your head, every constructor call in this module becomes predictable.

The four constructor calls in ex01

Fixed has four constructors after ex01. Run the procedure on each call the subject makes.

CallArgument typeFixed()Fixed(const int)Fixed(const float)Fixed(const Fixed &)Result
Fixed a;noneonlynot viablenot viablenot viabledefault
Fixed b( 10 );intnot viableexactconversionnot viableint constructor
Fixed c( 42.42f );floatnot viableconversionexactnot viablefloat constructor
Fixed d( b );Fixed lvaluenot viablenot viablenot viableexactcopy constructor
Fixed x( 10.0 );doublenot viableconversionconversionnot viableambiguous
measured The last row is a real compile error on this machine, error: call of overloaded 'Fixed(double)' is ambiguous. A bare decimal literal is a double, and double → int and double → float are both standard conversions of the same rank, so neither constructor is better. The subject's main never writes a bare decimal, always 42.42f with the suffix, and that suffix is the whole difference. The lab lets you feed it other literals and watch the table fill in.

Constness is a type too

ex02 declares max twice, once taking Fixed & and once taking const Fixed &. The subject then calls Fixed::max( a, b ) with a non const and b declared Fixed const. Run the procedure. The non const overload would need to bind b to a Fixed &, which would silently drop its constness, so the language refuses and that overload is not viable. The const overload binds both, since a non const object binds happily to a const reference, and it wins. It returns a const Fixed &.

measured Proof that the const overload was chosen. Trying to catch the result in a Fixed & fails with binding reference of type 'Fixed&' to 'const Fixed' discards qualifiers. Had the non const overload won, that line would compile.

This is what the module title means by ad-hoc polymorphism. One name, several behaviours, selected at compile time by the types of the arguments, constness included.

Operators are overloaded functions with unusual names

a + b is resolved exactly the same way. The compiler sees a binary + with a Fixed on the left, looks for a function named operator+ that it could call as a.operator+(b), and finds the member. ++a becomes a.operator++() and a++ becomes a.operator++(0), where the 0 is a dummy that exists only to make the two signatures different. None of this is a new mechanism. It is name lookup plus the procedure above.

Z5 · level zeroLife of an object

Every object has a beginning and an end, and the language decides both by rules, never by chance. Two rules cover everything in this module.

  • A named object dies at the closing brace of the scope that declared it. Objects in the same scope die in reverse order of construction.
  • An unnamed temporary dies at the end of the full expression that created it, which is the semicolon.

Anatomy of one statement

ex01 statement 5 does more than it looks like it does. a = Fixed( 1234.4321f ); produces three trace lines from one line of source, and the third is the one that surprises people.

The right hand side is evaluated first. Fixed( 1234.4321f ) is a constructor call with no name attached, so it builds a temporary at a fresh address. Then operator= copies its raw value into a, which already exists. Then the semicolon arrives, the full expression is over, and the temporary is destroyed. In the subject's expected output this is the lone Destructor called in the middle, before any value is printed.

measured · how we know it is a temporary Three independent tests. Giving it a name, Fixed t( 1234.4321f ); a = t;, produces the same constructor at the same address but the destructor moves to the end of the scope. Binding it to a non const reference fails with cannot bind non-const lvalue reference of type 'Fixed&' to an rvalue of type 'Fixed'. And binding it to a const Fixed & extends its life to the end of the scope, which only temporaries can have done to them. The tracer, ex01 statement 5, shows it appear in a separate lane and vanish.

The end of main

ex01 has four named objects, a b c d, constructed in that order. When main returns they are destroyed d c b a. Every destructor prints the same text, so in the real output only the order tells you which object each line belongs to. Five constructions in the program, five destructions. Four named plus one temporary, and the temporary is the one whose destructor came early because its lifetime ended early.

Where copy elision fits

The language permits the compiler to skip a copy when a function returns an object by value, constructing the result straight into its destination. GCC does this. ex02's Fixed const b( Fixed( 5.05f ) * Fixed( 2 ) ); shows no Copy constructor called for b: the float constructor that operator* calls runs at b's own address. This is why the subject's expected traces say “something similar”. Elision is allowed, not required, so two correct programs can print different numbers of lines.

Z6 · level zeroHanding objects around

Every parameter and every return type in this module is a decision between three options, and each option has a visible cost in the trace.

Parameters

FormWhat the callee getsTrace costUsed in this module
Fixed xa brand new copycopy constructor and later a destructorbsp(Point const a, ...), imposed by the subject, 24 of its 68 trace lines
Fixed &xthe caller's own object under another namenonemin(Fixed &a, Fixed &b), so it can hand back something modifiable
const Fixed &xthe caller's object, read onlynoneevery operator's right operand, the copy constructor, operator<<, the getters

Const reference is the default for anything you only read. It costs nothing, it accepts const objects, and it accepts temporaries, which plain Fixed & cannot. std::cout << Fixed(5) only compiles because operator<< takes a const reference.

Return types

FormMeaningWhere
Fixed by valuea new, independent resulta + b, a * b, postfix a++ which must return the old snapshot
Fixed &an existing object, usually *this or an argumentoperator= so x = y = z chains, prefix ++a, non const max
const Fixed &an existing object, read onlyconst min / max, Point::getX
the one rule that never breaks Never return a reference to a local. Postfix operator++ keeps its snapshot in a local named before, so it must return by value. Returning Fixed & to before would dangle the instant the function ended.

Why operator<< is not a member

A member operator always takes its own class as the left operand. In std::cout << a the left operand is the stream, so a member version would have to belong to std::ostream, which is not yours to edit. Making it a member of Fixed compiles but reverses the syntax to a << std::cout. Measured. So it is a free function, declared after the class in Fixed.hpp, and it needs no friend because toFloat is public. It returns std::ostream & so the next << in the chain has a stream to attach to. Changing that to void fails with invalid operands of types 'void' and ... to binary 'operator<<'.

Z7 · level zeroThe mechanical map

Every line of C++ in this module falls into one of these rows. When a piece of code feels opaque, find its row.

You writeWhat it meansWhat actually runsNew object?
Fixed a;new object, no argumentsFixed::Fixed()yes
Fixed b( 10 );new object from an intFixed::Fixed(const int)yes
Fixed c( 42.42f );new object from a floatFixed::Fixed(const float)yes
Fixed d( b ); or Fixed d = b;new object from an existing onecopy constructoryes
Fixed( 5.05f ) with no namea temporaryfloat constructor, then the destructor at the semicolonyes, briefly
a = b;overwrite an existing objecta.operator=(b)no
a.toFloat()ordinary member callFixed::toFloat with this → ano
a + boverloaded operatora.operator+(b), which constructs its resultyes, the result
a < boverloaded comparisona.operator<(b)no
++aprefix incrementa.operator++(), returns a itselfno
a++postfix incrementa.operator++(0), returns a snapshotyes, the snapshot
Fixed::max( a, b )static call, no thisthe overload that fits the constness of a and bno, returns a reference
std::cout << afree operatoroperator<<(std::cout, a)no
closing bracescope endsdestructors, reverse orderobjects removed

Z8 · level zeroVocabulary

WordMeaning here
classa description of a type, existing once, running nothing
object, instanceone actual thing built from the class, with its own state and address
statethe data inside one object, here a single int
member function, methodan operation in the class's catalogue, called through an object
thisinside a member function, a pointer to the object the call went through
constructorbegins an object's life and sets its state
copy constructorbegins a new object's life using an existing one
copy assignmentchanges an existing object to match another
destructorends an object's life, runs at the closing brace or the semicolon
Orthodox Canonical Formthe four above, all written explicitly
initializer listthe : _value(0) part, the only place a const member or a reference can be set
overloadingseveral functions sharing a name, told apart by parameters
overload resolutionthe compile time procedure that picks one
ambiguoustwo candidates tied at the same rank, a compile error
temporaryan unnamed object living until the end of its full expression
lvalue, rvalueroughly, something with a name and address versus something that is only a value
referenceanother name for an existing object, never a copy
const member functionone that promises not to modify this
static member functionone with no this, called through the class
copy elisionthe compiler skipping a permitted copy, constructing a result in place
compositiona class holding objects of another class, as Point holds two Fixed
invarianta rule about state the class keeps true, here “the number is _value / 256”
raw valuethe stored integer, before the invariant interprets it
epsilon, εthe smallest step the type can take, raw 1, which is 1/256

N1 · foundationsFixed point from zero

An int has 32 bits and a binary point sitting to the right of the last one. Fixed point moves that point. Fixed declares eight fractional bits, so it decides in advance that the low eight bits of the integer are the fraction and the upper twenty four are the whole part. The bits do not change. Their interpretation does.

value = _value / 28 = _value / 256 One unit of the number is 256 raw units. One raw unit is 1/256 = 0.00390625 of the number. That second fact is ε, the increment step in ex02.

Some raw values and what they mean

raw _valuebits, point shownnumberhow it got there
00.000000000Fixed a;
10.000000010.00390625++a from zero, one ε
1280.100000000.5Fixed(0.5f)
2561.000000001Fixed(1)
3841.100000001.5Fixed(1.5f)
25601010.0000000010Fixed(10)
1293101.000011015.05078125Fixed(5.05f), note the rounding
10860101010.0110110042.421875Fixed(42.42f)
−384two's complement−1.5Fixed(-1.5f)

What you buy and what you pay

Every fractional bit doubles the precision and halves the range. Eight bits give a step of 1/256 and a range of about ±8.39 million, since 231 / 256 is 8388608. A float spends its 32 bits differently, on a mantissa and a moving exponent, and gets a vast range with about seven significant digits everywhere. Fixed point gets the same absolute precision everywhere, which is why it suits audio and graphics, and it can be added and compared with plain integer instructions.

not every decimal survives 42.42 has no exact form in 1/256 steps. The nearest representable value is 42.421875, and that is what Fixed(42.42f) stores. The 0.001875 difference is not a bug in your code. It is the type's resolution. The same thing happens to 0.1 in a float, only with a different grid.

N2 · foundationsThree views of one object

One Fixed holding raw 1293 answers three different questions depending on which member you ask. Knowing which one std::cout << a uses ends most of the confusion about the printed output.

The conversion map

DirectionCodeRuleNotes
int → FixedFixed(const int v) : _value(v * 256)multiply by 256exact for every int in range, multiply not shift, see E1
float → Fixed_value(static_cast<int>(roundf(f * 256)))multiply, round, narrowrounding makes the conversion symmetric for negatives
Fixed → floatstatic_cast<float>(_value) / 256cast first, then dividewithout the cast this is integer division and the fraction is gone
Fixed → int_value / 256integer divisiontruncates toward zero, so −1.5 gives −1
Fixed → rawgetRawBits()no conversionthis is the stored integer, not a number in the type's units
the sentence that fixes it getRawBits shows you the box. toFloat and toInt tell you what the class says is in the box. operator<< asks toFloat.

N3 · foundationsWhere the digits come from

Why does a print as 1234.43 with two decimals, while c prints as 42.4219 with four? Because std::cout is not printing decimals. It is printing six significant digits, and the whole part uses some of them up.

ObjectLiteral× 256roundfraw / 2566 significant digits
c42.42f10859.521086042.42187542.4219
a1234.4321f316014.61763160151234.433593751234.43
b102560none, int path1010
ex02 b5.05f × 21293 and 512258610.101562510.1016
ε++ from zeroraw 10.003906250.00390625

Count the digits. 1 2 3 4 4 3 is six, so four before the point and two after. 4 2 4 2 1 9 is six, two before and four after. 0.00390625 fits in six because leading zeros do not count. None of this is the Fixed class deciding anything. Leave the stream's formatting alone, since the expected output relies on the default.

why the raw value is checked directly in the tests Several different raw values print the same six digits. A scale factor bug that produced raw 315999 instead of 316015 would print 1234.37, which looks plausible unless you already know the answer. The acceptance checks compare raw values, not printed strings, for exactly this reason.

N4 · foundationsArithmetic on raw values

One fact decides every arithmetic design choice in ex02. A raw value already carries the factor 256. So when you combine two raw values, you have to ask how many times that factor appears in the result.

Addition and subtraction carry it once

(a·256) + (b·256) = (a + b)·256 The sum of two raw values is already the raw value of the sum. No rescaling, and the operation is exact. result._value = _value + other._value;

Multiplication carries it twice

So the honest integer form is (_value * other._value) / 256, and it is right until the product no longer fits in 32 bits.

measured · where the integer form dies 181 × 181 gives 32761, correct. 182 × 182 gives −32412 instead of 33124. At 200 × 200 the raw operands are 51200 each and their product 2621440000 exceeds 2147483647, so it wraps negative and the result prints −25536. No warning at any level. A number type that fails above 181 is not a number type.

C++98 has no long long, and long is only guaranteed to be at least as wide as int, so there is no portable wider integer to reach for. That leaves floating point.

Fixed	Fixed::operator*(const Fixed &other) const
{
	return Fixed(this->toFloat() * other.toFloat());
}

Both operands go back to real values, multiply, and the float constructor scales and rounds the result. Two measured advantages beyond merely working. The range extends to the type's own limit, confirmed at 200 × 200 = 40000 and 70000 × 2 = 140000. And rounding beats truncation: 5.05 squared is raw 6530.66 exactly, the float path gives 6531, the integer form would give 6530.

the honest limit of the float path A float has a 24 bit mantissa, so raw values above 224, which is a number above 65536, are not all representable and results can be off by a few raw steps. Below that the float path is exact. Addition, subtraction and every comparison stay exact across the full int range because they never leave integer arithmetic. The subject never approaches these magnitudes, and the lab lets you find the edge yourself.

Division

Same shape as multiplication through float. The subject says a crash on division by zero is acceptable. This implementation does not crash. Measured: a float infinity comes out, roundf passes it through, and casting infinity to int is undefined behaviour that on this machine saturates to INT_MIN, printing -8.38861e+06, for 10/0, -10/0 and 0/0 alike. A silent wrong number rather than a crash. There is no guard because the subject waives it, and this is the sentence to have ready if asked.

Comparison

Compare the raw integers directly. Both sides carry the same positive constant, so the raw ordering and the real ordering always agree, and the comparison is exact. Converting to float to compare would introduce rounding into an operation that is currently perfect.

Epsilon and the increments

The smallest ε with 1 + ε > 1 is one raw unit. So ++ is _value += 1, a single integer step, never += 1.0 through the rounding path. From zero, one increment prints 0.00390625, two print 0.0078125, exactly as the subject's output shows.

E0 · exerciseex00 · My First Class in Orthodox Canonical Form

The class can only hold zero and the only way to change it is setRawBits. What is graded is the canonical form and the trace that proves which member ran when. The subject supplies both the program and the expected output, so this is a byte for byte matching exercise.

The class

class Fixed
{
private:
	int			_value;
	static const int	_fractionalBits = 8;

public:
	Fixed();
	Fixed(const Fixed &other);
	Fixed	&operator=(const Fixed &other);
	~Fixed();

	int	getRawBits(void) const;
	void	setRawBits(int const raw);
};

The canonical four come first and always in this order, so they can be checked at a glance in every class from here to module 09. The header needs no includes: it names only int and itself. setRawBits(int const raw) is the subject's own spelling, kept character for character, and it means the same as const int raw.

The trace, decoded

Default constructor calledFixed a;
Copy constructor calledFixed b( a );
Copy assignment operator called*this = other, inside that copy constructor
getRawBits member function calledother.getRawBits(), inside that assignment
Default constructor calledFixed c;
Copy assignment operator calledc = b;
getRawBits member function calledthe same read, of b this time
getRawBits member function calleda.getRawBits() in the first cout
0its return value, printed
getRawBits member function calledb.getRawBits()
0
getRawBits member function calledc.getRawBits()
0
Destructor calledc, reverse order of construction
Destructor calledb
Destructor calleda

Two things the trace encodes that the prose never states. The copy constructor delegates, which is the pair of lines after it. And the assignment operator reads through the getter, which is why a getRawBits line follows every assignment. Both are unusual choices made so that the output matches the subject exactly. Step through it in the tracer.

Why operator= returns Fixed &

Three candidates. void breaks a = b = c, since the inner assignment must produce something for the outer one to consume. By value compiles and chains but constructs a temporary on every assignment, and (a = b) = c would assign into that temporary and throw the result away. By reference chains with no copy and (a = b) = c modifies a. return *this; returns the object that was just assigned into.

The self assignment guard

if (this != &other)
	_value = other.getRawBits();

For this class the guard changes nothing, since assigning an int to itself is harmless. It is there for the class that owns a resource. The classic assignment operator releases what it holds and then copies from the source, and handed x = x without a guard it releases the resource and then reads from the thing it just freed. The habit costs one pointer comparison now. Note it compares addresses. Two objects holding the same value are different objects and must still be assignable.

Gotchas

  • Giving setRawBits a message adds a line the reference trace never has. It stays silent.
  • Copying _value directly in the copy constructor produces a shorter, also valid, trace that no longer matches the subject's.
  • Forgetting return *this; compiles with a warning at best and is undefined behaviour when the result is used.
  • Binding _fractionalBits to a reference is a link error, not a compile error.

E1 · exerciseex01 · Towards a more useful fixed point number class

Two converting constructors come in, two conversions go out, and operator<< makes it printable. The subject gives the program and its twenty line output, reproduced exactly.

The one change to code already written

if (this != &other)
	_value = other._value;

ex01's expected trace has no getRawBits line after either assignment, so the operator stops reading through the getter. Legal without friend, because access control is per class, not per object. The getter keeps its message; the class simply never calls it internally from here on, which also stops ex02's operators from burying the output.

The int constructor multiplies, it does not shift

Fixed::Fixed(const int value) : _value(value * (1 << _fractionalBits))

value << 8 looks like the idiomatic fixed point move and is undefined behaviour in C++98 for a negative value. What makes it nasty is that nothing tells you. Measured on −7: multiply gives −1792, shift gives −1792, clean under every warning flag. Undefined code that happens to produce the right answer is worse than a wrong answer, because there is no symptom until the compiler or platform changes. 1 << _fractionalBits alone is fine, since the left operand is the positive literal 1.

The float constructor rounds

Fixed::Fixed(const float value)
	: _value(static_cast<int>(roundf(value * (1 << _fractionalBits))))

roundf becomes authorized here. Rounding rather than truncating makes the conversion symmetric for negatives and the subject's digits depend on it. The static_cast<int> states the narrowing rather than leaving it implied.

toFloat, where the cast is load bearing

compiles clean, silently wrong
return _value / (1 << _fractionalBits);

Both operands are int, so this is integer division. The fraction is discarded before the result is widened on the way out. On raw 10860 it returns 42.

shipped
return static_cast<float>(_value) / (1 << _fractionalBits);

Cast first, then divide, and the division is floating point. Same raw value returns 42.4219.

This is the most likely way to end up with a plausible but wrong class. It compiles under -Werror, returns a float as promised, gets every whole number right, and only loses the fractions. A test using only integers would pass.

toInt divides

_value / 256 truncates toward zero. A right shift floors instead. On raw −384, which is −1.5, division gives −1 and the shift gives −2. Division matches what a plain float to int conversion does and keeps toInt symmetric with the constructor's multiplication. Both are implementation defined for negatives in strict C++98, so neither is provably portable, and the subject never exercises negatives.

Stream insertion

std::ostream	&operator<<(std::ostream &out, const Fixed &value)
{
	out << value.toFloat();
	return out;
}

A free function, for the reasons in Z6. Fixed.hpp gains exactly one include, <ostream>, because it names std::ostream. Not <iostream>, since the header needs the type and not the global stream objects.

Reading the output

Default constructor calledFixed a;
Int constructor calledFixed const b( 10 ); raw 2560
Float constructor calledFixed const c( 42.42f ); raw 10860
Copy constructor calledFixed const d( b );
Copy assignment operator calleddelegated from that copy constructor, no getter line now
Float constructor calledthe temporary in a = Fixed( 1234.4321f ); raw 316015
Copy assignment operator calledthe assignment into a
Destructor calledthe temporary dying at the semicolon
a is 1234.43316015 / 256 = 1234.43359375, six significant digits
b is 10
c is 42.421910860 / 256 = 42.421875
d is 10
a is 1234 as integertoInt truncates
b is 10 as integer
c is 42 as integer
d is 10 as integer
Destructor called × 4d, c, b, a

Why the conversions must be const

b, c and d are declared Fixed const and the main calls toInt on all three and prints them. Without const on toFloat, toInt, and the const Fixed & parameter of operator<<, the subject's own program does not compile.

E2 · exerciseex02 · Now we're talking

Sixteen operators and four static functions. The design is decided by N4. This section covers what is left, which is mostly about returning the right thing.

Comparisons

bool	Fixed::operator<(const Fixed &other) const	{ return _value < other._value; }

Six of these, one line each, all const, all on raw values. Exact by the argument in N4.

Prefix and postfix

Fixed	&Fixed::operator++(void)      { _value += 1; return *this; }

Fixed	Fixed::operator++(int)
{
	Fixed	before(*this);

	_value += 1;
	return before;
}

The unnamed int parameter exists only to make the two signatures differ. Leave it unnamed, since a named unused parameter trips -Wextra. Postfix must return by value: its contract is the value from before the change, a reference to *this would show the new value, and a reference to the local before would dangle. The tracer shows the snapshot appear at a reused address and die at the semicolon.

a real bug hiding in a test std::cout << q++ << q has unspecified evaluation order in C++98. The second read may happen before or after the increment. Every observation in the shipped main is its own statement for this reason.

min and max, and why both pairs must exist

static Fixed		&min(Fixed &a, Fixed &b);
static const Fixed	&min(const Fixed &a, const Fixed &b);

Each returns a reference to one of its arguments, never a copy. On a tie both return b, an arbitrary rule that the tests document by comparing addresses. The const pair alone would accept every call, so why write the other two? Because only the non const pair hands back something the caller can modify.

measured Fixed &biggest = Fixed::max(x, y); biggest.setRawBits(0); leaves y at 0. The reference aliased y, not a copy of it. That capability is the entire reason the non const overload exists, and returning by value would remove it while printing identical output.

Reading the filtered output

Every temporary prints its constructor and destructor, so the test sections are read with grep -v called. One artifact to know. In std::cout << "10 + 4 " << (ten + four) the label is inserted before the addition runs, the addition prints its own constructor line onto the same physical line, and the filter drops the whole line. The value then appears alone on the next line. That is why the arithmetic section reads as bare 14 6 40 2.5 after filtering.

E3 · exerciseex03 · BSP

A Point with two Fixed const coordinates, and a free function bsp that says whether a point is strictly inside a triangle. A point on an edge or at a vertex is outside.

The trap, and it is the whole lesson

The subject requires Fixed const members and Orthodox Canonical Form, which includes a copy assignment operator. Those two requirements conflict. A const member can be set only in a constructor's initializer list and never assigned afterwards, so an assignment operator physically cannot copy _x or _y.

Point	&Point::operator=(const Point &other)
{
	(void)other;
	return *this;
}
measured, both halves Omit the operator and assign anyway, and the compiler generated one is ill formed the moment it is used: non-static const member 'const Fixed Bare::_x', cannot use default assignment operator. Write the no op above, and a point of 8,9 assigned onto a point of 1,2 leaves it at 1,2. The (void)other; is load bearing, since -Werror=unused-parameter rejects the build without it.

This looks like an unfinished function and it is the only legal one. The design lesson underneath is that const members and value semantics do not mix. A type with const members can be copy constructed but never meaningfully assigned, so it cannot live in anything that reassigns its elements. The subject imposes the const to make that collision visible.

Point

Default constructor initializes both to 0 through the initializer list, selecting Fixed's int constructor. The float constructor selects the float one. The copy constructor copies both through the list, the same constraint seen from the working side. Point prints nothing. The two getters return const Fixed &, because returning by value would copy, every Fixed copy prints two lines, and cross calls the getters twelve times per invocation.

The sign test

static Fixed	cross(const Point &o, const Point &a, const Point &b)
{
	return (a.getX() - o.getX()) * (b.getY() - o.getY())
		- (a.getY() - o.getY()) * (b.getX() - o.getX());
}

For each edge, the two dimensional cross product of the edge vector with the vector from the edge's start to the point. Its sign says which side of that edge's line the point is on. Strictly inside means the same side of all three edges, so all three signs agree.

if (d1 == zero || d2 == zero || d3 == zero)
	return false;
if (d1 > zero && d2 > zero && d3 > zero)
	return true;
if (d1 < zero && d2 < zero && d3 < zero)
	return true;
return false;

The zero test is redundant to the compiler and informative to a reader. A zero fails both strict comparisons and would fall through to false anyway. Removing that line changes nothing, and that was verified by removing it. It stays because it is the only place the code visibly states the subject's boundary rule, in a project where comments are not allowed. Say exactly that if asked.

Accepting both all positive and all negative is necessary. The sign depends on the triangle's winding. Counter clockwise vertices give three positives for an interior point, the same vertices listed clockwise give three negatives. Rejecting one would return false for a point genuinely inside a triangle specified the other way round. Verified on both windings.

The cost of by value parameters

The subject's signature takes four Points by value, so each call copies four points, which is eight Fixed copies. One call emits 68 trace lines. Eight copy constructions and their eight delegated assignments, fifteen default constructions from Fixed result; inside operator-, six float constructions from operator*, one int construction for zero, and thirty destructions. Balanced. Passing by const reference would remove 24 lines. The subject fixed the signature, so the cost stays, and the tracer steps through every one of those lines with its origin named.

The precision caveat, which is not where you expect

Edge detection through the float path is robust. For a point exactly on an edge the two products inside cross are mathematically equal, both are correctly rounded from exactly representable operands, so they produce bit identical floats, the same raw integer, and a difference of exactly zero. Verified on all three edges and on a triangle of side 300. The hazard is upstream. Coordinates are quantized to 1/256 when a Point is built, so 4.999 becomes raw 1280, which is exactly 5.0, and a point the caller thinks is strictly inside can land on the hypotenuse before bsp ever sees it. bsp is right about the point it was given, and the point is not the one that was asked for. The resolution limit lives in the type.

The harness

Nineteen cases through a check helper printing PASS or FAIL, a summary line, and a nonzero exit on any failure. The summary exists because the trace output is enormous and a verdict at the very end is the only thing visible without scrolling. The harness was proven capable of failing by flipping every return false in bsp to true, which produced exactly twelve failures, the twelve cases whose expected answer is false.

D1 · defenseEval runbook

What to run, in order, and what you should see. Every command from the directory of that exercise.

Every exercise

  • make re compiles with zero warnings. Then make again reports nothing to be done.
  • c++ -Wall -Wextra -Werror -std=c++98 -pedantic *.cpp is also silent.
  • valgrind --leak-check=full ./<binary> reports zero leaks. The type allocates nothing, so the only heap use is the stream library's.
  • make fclean leaves exactly the submitted files.
  • No friend, no using namespace, no printf, no comments, no function bodies in headers. Include guards on every header.

ex00

  • ./fixed prints exactly the sixteen lines of the subject. The middle line under the copy constructor is present because the copy constructor delegates.
  • Be ready to point at the four canonical members in the header, in order.

ex01

  • ./fixed prints the subject's twenty lines. The lone destructor in the middle is the temporary.
  • Raw values if asked: 10 → 2560, 42.42f → 10860, 1234.4321f → 316015, −1.5f → −384.
  • Be ready to explain Fixed x(10.0) not compiling.

ex02

  • ./fixed | grep -v called starts with the subject's seven values then four labelled sections.
  • Be ready to show Fixed::max(a, b) choosing the const overload, and the non const one returning a modifiable reference.
  • Know the numbers: integer multiply dies at 182, float path exact below 65536, ε is 0.00390625, division by zero prints −8.38861e+06.

ex03

  • ./bsp | grep -v called prints nineteen PASS lines and SUMMARY 0 failures, exit status 0.
  • Be ready to defend the no op assignment operator in one sentence, and the redundant zero test in one more.
  • Know that one bsp call is 68 trace lines and why.

D2 · defenseQ&A drill

Grouped by theme. Open one, answer aloud, then read.

The object model

What is the difference between a class and an object?
The class is the description, existing once and running nothing. An object is one instance built from it, with its own state and its own address. Three Fixed objects hold three separate _value integers and share one copy of every function.
What is this? What is *this?
Inside a non static member function, this is a pointer to the object the call went through. *this is that object itself. a.toFloat() runs toFloat with this pointing at a.
Copy constructor versus copy assignment, in one sentence each.
The copy constructor creates a new object from an existing one. The assignment operator changes an object that already exists. Fixed b(a) is the first, c = b is the second, and Fixed d = b is the first despite the equals sign.
Why does your copy constructor produce three trace lines?
Because it delegates with *this = other, so the assignment operator prints its line, and in ex00 that operator reads through getRawBits, which prints a third. That reproduces the subject's trace exactly, and the subject marks the middle line as one that may be missing for implementations that copy the member directly.
Why does operator= return Fixed &?
So a = b = c chains, and so (a = b) = c modifies a rather than a temporary. By reference means no copy. return *this returns the object just assigned into.
What does the this != &other guard protect against?
Self assignment, x = x. Harmless for an int member, but for a class that releases its resource before copying from the source it prevents a use after free. It compares addresses, not values, because two different objects holding the same value must still be assignable.
Why can operator= read other._value when _value is private?
Access control is per class, not per object. Any member function of Fixed may access the private members of any Fixed. This is why no friend is ever needed here.
What does the trailing const on toFloat() const mean?
The function promises not to modify this. The compiler enforces it, and it is what lets the function be called on a const Fixed, which the subject's main does.
Why is _fractionalBits static? Why const?
Every Fixed uses the same format, so the value belongs to the class rather than being repeated in each object. Const because it must never change. Its value is the literal 8 because the subject says so.
Why is min static?
It compares two objects on equal terms. It is nobody's method, so it has no this and both arguments arrive explicitly. It is called as Fixed::min(a, b).

Lifetime and temporaries

Why is there a lone Destructor called in the middle of ex01's output?
It is the temporary Fixed( 1234.4321f ). A temporary dies at the end of the full expression that created it, the semicolon, so it is constructed, assigned from and destroyed within one statement. Named objects cannot die before the closing brace.
How do you know it is a temporary and not one of the named objects?
It has no name. It refuses to bind to a non const reference. Binding it to a const reference extends its life, which only temporaries get. And giving it a name moves its destructor to the end of the scope while nothing else changes. All four measured.
In what order are a b c d destroyed?
Reverse order of construction, d c b a. Every destructor prints the same text, so only position tells them apart.
Why does ex02 show no copy constructor for b in Fixed const b( Fixed(5.05f) * Fixed(2) )?
Copy elision. The compiler constructed operator*'s return value directly in b's storage. The language permits skipping that copy, which is why the subject says the output should be “something similar” rather than identical.
Which operand of Fixed(5.05f) * Fixed(2) is constructed first?
On this compiler, Fixed(2), the right one. C++98 leaves operand evaluation order unspecified. Code must never depend on it.
Why must postfix operator++ return by value?
Its contract is the old value. It snapshots into a local, increments, and returns the snapshot. A reference to *this would show the new value, and a reference to the local would dangle. By value is the only correct option, and it costs one construction and one destruction that prefix never pays.

Overloading

How does the compiler choose between Fixed(int) and Fixed(float) for Fixed(10)?
Overload resolution. Both are viable, but int → int is an exact match and int → float is a conversion. Exact beats conversion, so the int constructor runs.
Why does Fixed x(10.0) fail to compile?
A bare decimal is a double. double → int and double → float are both standard conversions of equal rank, so the call is ambiguous. The subject's main always writes the f suffix. Measured error text is call of overloaded 'Fixed(double)' is ambiguous.
Which max does Fixed::max( a, b ) call when b is const?
The const overload. Binding a const Fixed to a Fixed & would discard its constness, so the non const overload is not viable. Proof: binding the result to a Fixed & fails with discards qualifiers.
Why write both const and non const min / max?
The const pair alone accepts every call but can only return something read only. The non const pair returns a reference the caller can modify, aliasing one of the arguments. Zeroing through Fixed::max(x, y) changed y, measured. That is the ad-hoc polymorphism the module is named for.
What is a + b really?
a.operator+(b), an ordinary member function call with a special name, resolved by the same procedure as any overload. It constructs and returns a new Fixed.
Why is operator<< not a member? Why no friend?
A member operator takes its own class as the left operand, and the left operand of std::cout << a is the stream, which is not yours to extend. As a member of Fixed it would only work as a << std::cout. It needs no friend because it only calls the public toFloat.
Why does operator<< return std::ostream &?
So the next << in the chain has a stream to attach to. With void, std::cout << a << std::endl fails to compile. By reference because streams are not copyable.
What is the dummy int in operator++(int)?
A marker that distinguishes the postfix signature from the prefix one. It is never given a meaningful value, so it is left unnamed to avoid an unused parameter error.

The number

What is the raw value? What does eight fractional bits mean?
The raw value is the stored integer. Eight fractional bits means the low eight bits are the fraction, so the scale is 28 = 256 and the number is _value / 256.
Why does a print as 1234.43 and c as 42.4219?
std::cout prints six significant digits by default, not six decimals. 1234.43359375 spends four digits before the point, 42.421875 spends two. The class decides nothing about formatting.
Why multiply by 256 instead of shifting left by 8?
Left shifting a negative signed integer is undefined behaviour in C++98. Multiplication is fully defined and compiles to the same instruction. Measured on −7, both give −1792 today, which is exactly why the undefined version is dangerous: there is no symptom.
Why the cast in toFloat?
Without it, _value / 256 is integer division and the fraction is discarded before the widening. It compiles clean and returns 42 for raw 10860 instead of 42.4219. Cast first, then divide.
Why does toInt divide instead of shifting right?
For negatives they disagree by one: division truncates toward zero, a shift floors. Division matches a plain float to int conversion and is symmetric with the constructor's multiplication. Both are implementation defined for negatives in C++98, so neither is provably portable.
Why does multiplication go through float?
The product of two raw values carries the factor 256 twice and overflows a 32 bit int at a value of 182, measured. C++98 has no guaranteed wider integer. The float path extends the range to the type's own limit and rounds correctly instead of truncating.
Where does the float path lose exactness?
Above raw 224, a number of about 65536, since a float has a 24 bit mantissa. Below that it is exact. Addition, subtraction and comparison are exact everywhere because they never leave integer arithmetic.
What happens on division by zero?
No crash. The float division gives infinity, roundf passes it through, and casting infinity to int is undefined behaviour that on this machine saturates to INT_MIN, printing −8.38861e+06 for any dividend. The subject waives the requirement, so there is no guard, and this is the honest answer.
What is ε and why is ++ a raw increment?
One raw unit, 1/256 = 0.00390625, the smallest step with 1 + ε > 1. ++ does _value += 1, a single exact integer step, never a float addition through the rounding path.
Why compare raw values rather than toFloat results?
Both sides carry the same positive constant, so raw ordering equals real ordering and the comparison is exact. A float round trip could merge two values one raw step apart.

Point and bsp

Why does Point::operator= do nothing?
Because its members are Fixed const and a const member can only be set in an initializer list. Canonical form requires the operator to exist, so the only legal body discards its argument and returns *this. Omit it and assignment fails with cannot use default assignment operator. Write it and assignment silently changes nothing, measured.
Why (void)other;?
Without it, -Werror=unused-parameter rejects the build. The cast explicitly discards an argument the function must ignore.
Why do the getters return const Fixed &?
Returning by value would copy, every copy prints two trace lines, and cross calls the getters twelve times per call. A const reference costs nothing. They exist because bsp is outside the class and friend is forbidden.
Explain the algorithm.
For each edge, the cross product of the edge vector with the vector from the edge's start to the point gives the side of that edge's line the point is on. Strictly inside means the same side for all three edges, so the three signs agree. A zero means on an edge's line, which the subject defines as outside.
Why accept both all positive and all negative?
The sign depends on the triangle's winding order. The same three vertices listed clockwise flip every sign. Accepting only one would reject interior points of clockwise triangles. Verified with both windings.
Isn't the zero test redundant?
Yes, to the compiler. A zero fails both strict comparisons and falls through to false. It is kept because it is the only place the code visibly states the subject's boundary rule, and comments are not allowed. Removing it was tested and changes nothing.
Why 68 trace lines for one call?
Four Points by value is eight Fixed copies, each delegating, 16 lines. zero is one. Each cross creates six intermediates and their destructors, 13 lines, three times. Then four locals and eight parameters die. Passing by const reference would save 24 lines, but the subject fixed the signature.
Could a point near an edge be misclassified?
Not by the sign test, whose zero is robust because equal products round identically. The hazard is upstream: coordinates are quantized to 1/256 at construction, so 4.999 becomes exactly 5.0 and may land on an edge before bsp sees it. The resolution limit lives in the type.

D3 · defenseLive modification drills

Evaluators may ask for a small change on the spot. Each of these should take under five minutes. Know where the change lands and what else it touches.

Asked toChangeWatch out for
make the copy constructor not delegatereplace : _value(0) and *this = other with : _value(other._value)the ex00 trace loses two lines, so the subject's output no longer matches byte for byte, which the subject allows
use 16 fractional bitschange the literal 8range drops to ±32768, ε becomes 1/65536, every expected number in the tests changes, integer multiply overflows even sooner
make toInt round instead of truncatereturn static_cast<int>(roundf(toFloat()));42.42 still gives 42, but 42.6 gives 43, and the ex01 output changes for values with fraction ≥ .5
add operator+=member returning Fixed &, _value += other._value; return *this;by reference, since it modifies *this, same shape as prefix ++
add unary minusFixed operator-() const returning a Fixed with raw -_valueby value, it is a new object; distinct from binary minus by having no parameter
make multiplication exactuse a long intermediate, static_cast<long>(_value) * other._value / 256long is only guaranteed as wide as int, so say this is exact on this platform, not by the standard
make bsp count edge points as insidedrop the zero test and change > to >= and < to <=a degenerate collinear triangle now returns true for points on its line
let Point be assignableremove const from the members, give operator= a real bodythe subject explicitly requires the const, so say so before changing it
pass Points by const reference to bspchange the four parameters68 trace lines become 44, and the subject's signature is no longer matched

D4 · defenseChoices in this code that invite a question

  • The copy constructor initializes _value to 0 before delegating. Redundant with the assignment that follows, kept so the object is never in an indeterminate state, and so the class follows the rule that members are initialized in the list.
  • main.cpp in ex00 and ex01 uses the subject's brace style. Transcribed verbatim because the deliverable is a byte for byte output match. Deliberate inconsistency with the rest of the repository.
  • ex02's main is long. The subject says to test every feature. Five labelled sections, each observation its own statement.
  • The zero test in bsp. Redundant and kept as the visible statement of the boundary rule.
  • getRawBits still prints in ex01 onward. The subject never withdraws the message. The class simply stops calling it internally.
  • Multiplication and division through float, addition on raw. Addition needs no rescaling and stays exact. Multiplication needs it and overflows in integer form at 182.
  • No guard on division by zero. The subject waives it. The observed result is a saturated INT_MIN, not a crash.
  • <ostream> rather than <iostream> in the header. The header needs the type, not the global stream objects.
  • Tie rule in min and max returns the second argument. Arbitrary, documented by the tests.
↑