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
| Exercise | What is added | The idea being taught | The trap |
|---|---|---|---|
| ex00 | Orthodox Canonical Form and two raw accessors | construction, copying, assignment and destruction are four distinct events, and the trace proves which one ran | the copy constructor and the assignment operator are not the same thing |
| ex01 | int and float constructors, toFloat, toInt, stream insertion | a class can pretend to be a number, and overload resolution picks the constructor | an integer division hiding inside toFloat, and a bare 10.0 literal that is ambiguous |
| ex02 | sixteen operators and four static functions | operators are ordinary member functions with special names, and constness alone can select an overload | multiplication on raw values overflows at 182 |
| ex03 | Point with Fixed const members, bsp | composition, const members, and a signature that copies by value | const 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, nousing namespace, no STL containers, no C++11.roundffrom<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 setRawBitsFixed.cppthe six member bodies, each special member prints its line, operator= reads through the gettermain.cppthe subject's program verbatim, because the subject also supplies the expected outputMakefilesame shape as every module, NAME = fixedex01/ 4 files · ./fixed
Fixed.hppgains <ostream>, the two converting constructors, toFloat, toInt, and the free operator<< declared after the classFixed.cppgains <cmath> for roundf; operator= now reads other._value directlymain.cppsubject's program verbatimex02/ 4 files · ./fixed
Fixed.hppadds the sixteen operators and the four static min / max declarationsFixed.cppcomparisons on raw values, + and - on raw values, * and / through float, prefix and postfix increments, min and maxmain.cppthe subject's snippet plus four labelled test sections, because this subject says to test every featureex03/ 7 files · ./bsp
Fixed.hpp / .cppbyte identical to ex02Point.hpp / .cpptwo Fixed const members, canonical four, float constructor, two getters returning const references, and the bsp prototypebsp.cppa file local cross helper and bsp itselfmain.cppa self checking harness of nineteen cases with a summary line and a nonzero exit on failure01 · 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.
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.
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.
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.
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.
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.
| Call | Argument type | Fixed() | Fixed(const int) | Fixed(const float) | Fixed(const Fixed &) | Result |
|---|---|---|---|---|---|---|
Fixed a; | none | only | not viable | not viable | not viable | default |
Fixed b( 10 ); | int | not viable | exact | conversion | not viable | int constructor |
Fixed c( 42.42f ); | float | not viable | conversion | exact | not viable | float constructor |
Fixed d( b ); | Fixed lvalue | not viable | not viable | not viable | exact | copy constructor |
Fixed x( 10.0 ); | double | not viable | conversion | conversion | not viable | ambiguous |
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 &.
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.
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
| Form | What the callee gets | Trace cost | Used in this module |
|---|---|---|---|
Fixed x | a brand new copy | copy constructor and later a destructor | bsp(Point const a, ...), imposed by the subject, 24 of its 68 trace lines |
Fixed &x | the caller's own object under another name | none | min(Fixed &a, Fixed &b), so it can hand back something modifiable |
const Fixed &x | the caller's object, read only | none | every 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
| Form | Meaning | Where |
|---|---|---|
Fixed by value | a new, independent result | a + b, a * b, postfix a++ which must return the old snapshot |
Fixed & | an existing object, usually *this or an argument | operator= so x = y = z chains, prefix ++a, non const max |
const Fixed & | an existing object, read only | const min / max, Point::getX |
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 write | What it means | What actually runs | New object? |
|---|---|---|---|
Fixed a; | new object, no arguments | Fixed::Fixed() | yes |
Fixed b( 10 ); | new object from an int | Fixed::Fixed(const int) | yes |
Fixed c( 42.42f ); | new object from a float | Fixed::Fixed(const float) | yes |
Fixed d( b ); or Fixed d = b; | new object from an existing one | copy constructor | yes |
Fixed( 5.05f ) with no name | a temporary | float constructor, then the destructor at the semicolon | yes, briefly |
a = b; | overwrite an existing object | a.operator=(b) | no |
a.toFloat() | ordinary member call | Fixed::toFloat with this → a | no |
a + b | overloaded operator | a.operator+(b), which constructs its result | yes, the result |
a < b | overloaded comparison | a.operator<(b) | no |
++a | prefix increment | a.operator++(), returns a itself | no |
a++ | postfix increment | a.operator++(0), returns a snapshot | yes, the snapshot |
Fixed::max( a, b ) | static call, no this | the overload that fits the constness of a and b | no, returns a reference |
std::cout << a | free operator | operator<<(std::cout, a) | no |
| closing brace | scope ends | destructors, reverse order | objects removed |
Z8 · level zeroVocabulary
| Word | Meaning here |
|---|---|
| class | a description of a type, existing once, running nothing |
| object, instance | one actual thing built from the class, with its own state and address |
| state | the data inside one object, here a single int |
| member function, method | an operation in the class's catalogue, called through an object |
this | inside a member function, a pointer to the object the call went through |
| constructor | begins an object's life and sets its state |
| copy constructor | begins a new object's life using an existing one |
| copy assignment | changes an existing object to match another |
| destructor | ends an object's life, runs at the closing brace or the semicolon |
| Orthodox Canonical Form | the four above, all written explicitly |
| initializer list | the : _value(0) part, the only place a const member or a reference can be set |
| overloading | several functions sharing a name, told apart by parameters |
| overload resolution | the compile time procedure that picks one |
| ambiguous | two candidates tied at the same rank, a compile error |
| temporary | an unnamed object living until the end of its full expression |
| lvalue, rvalue | roughly, something with a name and address versus something that is only a value |
| reference | another name for an existing object, never a copy |
| const member function | one that promises not to modify this |
| static member function | one with no this, called through the class |
| copy elision | the compiler skipping a permitted copy, constructing a result in place |
| composition | a class holding objects of another class, as Point holds two Fixed |
| invariant | a rule about state the class keeps true, here “the number is _value / 256” |
| raw value | the 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.
Some raw values and what they mean
raw _value | bits, point shown | number | how it got there |
|---|---|---|---|
| 0 | 0.00000000 | 0 | Fixed a; |
| 1 | 0.00000001 | 0.00390625 | ++a from zero, one ε |
| 128 | 0.10000000 | 0.5 | Fixed(0.5f) |
| 256 | 1.00000000 | 1 | Fixed(1) |
| 384 | 1.10000000 | 1.5 | Fixed(1.5f) |
| 2560 | 1010.00000000 | 10 | Fixed(10) |
| 1293 | 101.00001101 | 5.05078125 | Fixed(5.05f), note the rounding |
| 10860 | 101010.01101100 | 42.421875 | Fixed(42.42f) |
| −384 | two's complement | −1.5 | Fixed(-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.
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
| Direction | Code | Rule | Notes |
|---|---|---|---|
| int → Fixed | Fixed(const int v) : _value(v * 256) | multiply by 256 | exact for every int in range, multiply not shift, see E1 |
| float → Fixed | _value(static_cast<int>(roundf(f * 256))) | multiply, round, narrow | rounding makes the conversion symmetric for negatives |
| Fixed → float | static_cast<float>(_value) / 256 | cast first, then divide | without the cast this is integer division and the fraction is gone |
| Fixed → int | _value / 256 | integer division | truncates toward zero, so −1.5 gives −1 |
| Fixed → raw | getRawBits() | no conversion | this is the stored integer, not a number in the type's units |
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.
| Object | Literal | × 256 | roundf | raw / 256 | 6 significant digits |
|---|---|---|---|---|---|
c | 42.42f | 10859.52 | 10860 | 42.421875 | 42.4219 |
a | 1234.4321f | 316014.6176 | 316015 | 1234.43359375 | 1234.43 |
b | 10 | 2560 | none, int path | 10 | 10 |
ex02 b | 5.05f × 2 | 1293 and 512 | 2586 | 10.1015625 | 10.1016 |
| ε | ++ from zero | raw 1 | 0.00390625 | 0.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.
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
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.
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.
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
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
setRawBitsa message adds a line the reference trace never has. It stays silent. - Copying
_valuedirectly 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
_fractionalBitsto 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
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.
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
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.
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.
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;
}
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 recompiles with zero warnings. Thenmakeagain reports nothing to be done.c++ -Wall -Wextra -Werror -std=c++98 -pedantic *.cppis 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 fcleanleaves exactly the submitted files.- No
friend, nousing namespace, noprintf, no comments, no function bodies in headers. Include guards on every header.
ex00
./fixedprints 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
./fixedprints 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 calledstarts 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 calledprints nineteenPASSlines andSUMMARY 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
bspcall 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?
Fixed objects hold three separate _value integers and share one copy of every function.What is this? What is *this?
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.
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?
*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 &?
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?
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?
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?
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?
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?
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?
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?
In what order are a b c d destroyed?
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) )?
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?
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?
*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)?
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?
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?
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?
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?
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 &?
<< 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)?
The number
What is the raw value? What does eight fractional bits mean?
_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?
Why the cast in toFloat?
_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?
Why does multiplication go through float?
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?
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?
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?
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?
Point and bsp
Why does Point::operator= do nothing?
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;?
-Werror=unused-parameter rejects the build. The cast explicitly discards an argument the function must ignore.Why do the getters return const Fixed &?
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.
Why accept both all positive and all negative?
Isn't the zero test redundant?
Why 68 trace lines for one call?
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?
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 to | Change | Watch out for |
|---|---|---|
| make the copy constructor not delegate | replace : _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 bits | change the literal 8 | range drops to ±32768, ε becomes 1/65536, every expected number in the tests changes, integer multiply overflows even sooner |
make toInt round instead of truncate | return 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 minus | Fixed operator-() const returning a Fixed with raw -_value | by value, it is a new object; distinct from binary minus by having no parameter |
| make multiplication exact | use a long intermediate, static_cast<long>(_value) * other._value / 256 | long 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 inside | drop the zero test and change > to >= and < to <= | a degenerate collinear triangle now returns true for points on its line |
let Point be assignable | remove const from the members, give operator= a real body | the subject explicitly requires the const, so say so before changing it |
pass Points by const reference to bsp | change the four parameters | 68 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
_valueto 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.cppin 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
mainis 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. getRawBitsstill 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
minandmaxreturns the second argument. Arbitrary, documented by the tests.