This project has been created as part of the 42 curriculum by baelgadi.
Casts and type conversion
Half the systems in my body read me as a man while the other reads me as hardware.
Same bits but 2 different types, and neither one is lying.
A cast is exactly that: telling the compiler which of my 2 selves it should be looking at.
Pick the wrong one and nothing crashes right away which is exactly the problem.
|
CPP Module 06:
3 exercises. C++98 standard only. Compiler: c++ · Flags: -Wall -Wextra -Werror · Must compile with -std=c++98Exercise 00: Conversion of scalar types · ex00/ · ScalarConverter, literal detection, static_castExercise 01: Serialization · ex01/ · Serializer, uintptr_t, reinterpret_castExercise 02: Identify real type · ex02/ · Base/A/B/C, dynamic_cast, pointer vs referenceForbidden: using namespace (-42) · friend (-42) · printf · *alloc · free · STL containers · STL algorithms · function bodies in headers · std::stringstream (ex00) · <typeinfo> (ex02)Required: Orthodox Canonical Form on every class · ScalarConverter and Serializer must be impossible to instantiate · every conversion goes through an explicit C++ cast |
In C we had 1 tool: (int)x and it did whatever it took to shut the compiler up. It never told you which one of these four jobs it was actually doing deep down.
(char)d // could be a value conversion...
(Data*)raw // ...or a raw bit reinterpretation...
(char*)str // ...or throwing away a const...
(Dog*)animal // ...or a downcastC++ splits that into 4 keywords, and the whole point is that the name of the cast is the documentation.
| Cast | What it does | Checked at | Cost at runtime | In this module |
|---|---|---|---|---|
static_cast |
Related types e.g double → int or int → char |
Compile time | None | ex00 |
reinterpret_cast |
Same bits, read them as something else | Never really | None | ex01 |
dynamic_cast |
Downcast in a polymorphic hierarchy | Run time | RTTI lookup | ex02 |
const_cast |
Adds or removes const or volatile |
Compile time | None |
3 of those 4 are free and unverified. Only dynamic_cast actually goes and asks the object what it is.
[!NOTE] A C style cast tries
const_cast, thenstatic_cast, thenreinterpret_cast, in order, and takes the first one that compiles.
’Means typo can silently downgrade a value conversion into a bit reinterpretation. The named casts can’t do that to you.
[!WARNING]
static_castwill happily narrow.static_cast<int>(1e20)is undefined behaviour.
Which is exactly why ex00 range checks before it casts. Never after.
Before I route power anywhere, I run a diagnostic on what I’m actually holding.
ScalarConverter decides what the string is before it decides what to do with it.
enum LiteralType { PSEUDO_LIT, CHAR_LIT, INT_LIT, FLOAT_LIT, DOUBLE_LIT, INVALID_LIT };
static LiteralType detectType(const std::string & s)
{
if (isPseudoLiteral(s))
return PSEUDO_LIT;
if (isCharLiteral(s))
return CHAR_LIT;
if (isIntLiteral(s))
return INT_LIT;
if (isFloatLiteral(s))
return FLOAT_LIT;
if (isDoubleLiteral(s))
return DOUBLE_LIT;
return INVALID_LIT;
}The order matters, ’coz nan has to be caught before anything tries to read it as a number, and 'c' before anything tries to read those quotes as digits.
Then everything, and I mean everything, is funnelled into a double:
double d = 0;
//...
printChar(d);
printInt(d);
printFloat(d, pseudo);
printDouble(d, pseudo);That’s the entire architecture. A double is the widest pivot available in C++98 that could hold a char, an int and a float without losing anything that matters here.
| Input type | How it reaches the double |
Note |
|---|---|---|
'c' |
static_cast<double>(literal[1]) |
The char between the quotes |
42 |
std::strtod |
std::stoi is C++11 |
4.2f |
strtod → static_cast<float> → back to double |
The float round trip is deliberate |
4.2 |
std::strtod |
Straight through |
That float round trip is doing real work:
case FLOAT_LIT:
d = static_cast<double>(static_cast<float>(std::strtod(literal.c_str(), NULL)));[!NOTE]
std::strtofwould have been 1 call instead of 3 casts, butstrtofis C99/C++11.strtodis C++98.
Narrowing the full precisiondoubledown tofloatrounds to infinity at exactly the same thresholdstrtofwould have, and widening it back keeps that infinity.
[!IMPORTANT] A literal has to have a digit in it, not just a dot.
.fand+.fare not C++ literals, soisFloatLiteraltrackshasDotANDhasDigit.
Had we drop the digit check,.fwould have silently converted to0.0f, which is a wrong answer.
doublenan, +inf, -inf and their f variants are not values you can parse. They’re states.
They get flagged on the way in and printed from the flag on the way out, because the double itself can no longer tell us which spelling it came from.
static bool isNaN(double d)
{
return d != d;
}
static bool isPosInf(double d)
{
return d > std::numeric_limits<double>::max();
}d != d is only ever true for NaN, since it’s the one value that isn’t equal to itself. And anything strictly greater than the largest finite double has nowhere else to be but infinity.
Both of those are pure C++98. std::isnan and std::isinf are C99.
Once we’re holding NaN or infinity, 2 of the 4 outputs simply cannot exist:
nan |
+inf |
2147483648 |
-1 |
128 |
|
|---|---|---|---|---|---|
char |
impossible | impossible | impossible | impossible | impossible |
int |
impossible | impossible | impossible | -1 |
128 |
float |
nanf |
+inff |
2147483648.0f |
-1.0f |
128.0f |
double |
nan |
+inf |
2147483648.0 |
-1.0 |
128.0 |
[!WARNING]
std::isprint(c)with a plaincharis undefined behaviour for negative values. The argument has to be representable asunsigned char, so we usestatic_cast<unsigned char>on every<cctype>call in the file.
There is no such thing as a ScalarConverter. Nobody owns one, nobody stores one.
class ScalarConverter
{
public:
static void convert(const std::string & literal);
private:
ScalarConverter();
ScalarConverter(const ScalarConverter & src);
ScalarConverter & operator=(const ScalarConverter & src);
~ScalarConverter();
};All 4 are declared and never defined, and they’re private.
Declared, so the compiler doesn’t generate the default versions.
Private, so nothing outside the class can call them anyway.
ScalarConverter sc; doesn’t compile.
A namespace |
A class with private constructors | |
|---|---|---|
| Can it be instantiated? | Nothing to instantiate | No, and the compiler says why |
| Can it have private helpers? | Only via static at file scope |
Yes, private members |
| Reopenable elsewhere? | Yes, anyone can add to it | No, it’s closed |
| What the subject asks for | — | This |
[!NOTE] C++11 would write
= deleteon those four lines.
In C++98, “declare it private and never define it” is the idiom.
reinterpret_cast and uintptr_tAn address is a number. It has always been, and reinterpret_cast is simply the moment we stop pretending otherwise.
uintptr_t Serializer::serialize(Data* ptr)
{
return reinterpret_cast<uintptr_t>(ptr);
}
Data* Serializer::deserialize(uintptr_t raw)
{
return reinterpret_cast<Data*>(raw);
}2 lines, zero instructions generated. Nothing moves, nothing is copied, no bit changes.
The only thing that changed is which type the compiler thinks it’s looking at.
uintptr_t is the specific point of the exercise: an unsigned integer type that’s guaranteed wide enough to hold a pointer and give it back unharmed.
# include <stdint.h> // <cstdint> is C++11The round trip has to survive both halves of the test:
uintptr_t raw = Serializer::serialize(&original);
Data *recovered = Serializer::deserialize(raw);
std::cout << (recovered == &original
? GREEN " check = OK (same address)\n" RESET
: RED " check = KO (different address)\n" RESET)
<< std::endl;[!WARNING]
reinterpret_castpromises the round trip. It promises absolutely nothing about what lives at that address.
If we deserialize a number we made up, or the address of an object that’s already been destroyed, we would get aData*that compiles perfectly and reads garbage.
[!NOTE] This is “serialization” in the narrow, in-process sense: the integer is only meaningful inside the running program that produced it. Write it to a file, restart, read it back, and it points at nothing.
Real serialization copies the contents, not the address.
dynamic_cast, the self diagnosticEvery other cast in this module trusts us. Not this one though.
Base * generate(void)
{
int random = std::rand() % 3;
if (random == 0)
return new A();
if (random == 1)
return new B();
return new C();
}The caller gets a Base* and genuinely does not know what’s underneath it.
The answer only exists at runtime so only a runtime tool can find it.
void identify(Base* p)
{
if (dynamic_cast<A*>(p))
std::cout << "A" << std::endl;
else if (dynamic_cast<B*>(p))
std::cout << "B" << std::endl;
else if (dynamic_cast<C*>(p))
std::cout << "C" << std::endl;
else
std::cout << "Unknown" << std::endl;
}because there is no null reference to hand back:
try
{
(void)dynamic_cast<A&>(p);
std::cout << "A" << std::endl;
return ;
}
catch(const std::exception& e) {}The subject forbids <typeinfo> and std::bad_cast is declared in <typeinfo>.
Catching const std::exception & instead of const std::bad_cast & is necessary.
Also, very important:
class Base
{
public:
virtual ~Base();
};Without that virtual function. Base has no vtable, no RTTI, and dynamic_cast doesn’t compile at all.
It’s also what makes delete obj; through a Base* legal in the first place (CPP04).
[!IMPORTANT]
Base::~Base() {}lives inBase.cpp, not in the header. “No function implementation in a header” applies to an empty destructor too
[!NOTE]
(void)on the reference cast is there because the result is never used, only the absence of a throw is. Without it-Wunused-valuebecomes-Werror
# ──── ex00 ────
# Verify: the 3 subject examples print exactly what the PDF shows
# Verify: '.f', '+.f' and '-.f' are rejected, while '4.f', '.5' and '5.' still convert
cd ex00 && make
./convert 0 ; ./convert nan ; ./convert 42.0f
./convert .f ; ./convert 4.f ; ./convert .5
./convert 999999999999999999999999999999999999999.0f
./convert "'c'" ; ./convert 2147483648 ; ./convert 42abc
valgrind --leak-check=full ./convert 42.0f
# ──── ex01 ────
# Verify: the recovered pointer compares equal to the original one
cd ex01 && make
./serializer
./serializer | grep check
valgrind --leak-check=full ./serializer
# ──── ex02 ────
# Verify: the pointer line and the reference line always agree on the same letter
# Verify: 2 runs launched back to back do not produce the same sequence
cd ex02 && make
./identify
./identify | tr -d '\n' ; echo ; ./identify | tr -d '\n' ; echo
valgrind --leak-check=full ./identifystatic_castreinterpret_castconst_castdynamic_caststd::uintptr_tstd::bad_caststd::strtodAnybody can put a cast in front of a value and make the error go away but that ain’t the skill.
The skill is being the one in the room who can say which of the 4 you reached for, what it checked, and what it didn’t.
Booyah.