← thecodex.expert · The Codex Family of Knowledge
Tier 0 · Absolute Beginner · C++ Project

Temperature Converter

Convert between Celsius, Fahrenheit, and Kelvin using std::optional for a conversion that might not exist. Teaches modern C++'s answer to a nullable return value.

🧠 Teaches how to think spoonfed, every age Last verified:

1 The Problem

We want a small program that converts a temperature from Celsius to Fahrenheit, or the other way around. The user picks which direction, types a number, and the program does the maths and shows the answer. Simple — but it teaches the core shape of every calculator-style tool: ask, calculate, show.

Where this shows up in real life: every app that takes a number and transforms it — a currency converter, a tip calculator, a unit converter, a tax estimator — is doing exactly this. Take input, apply a formula, return a result. Master the pattern here and you can build any of them.

2 How to Think About It

Six named conversions, one function. The interesting design question is what to return when the caller asks for a conversion that does not exist.

The plan — in plain English
1. Read a choice (like CtoF) and a value. → 2. Look up which formula the choice names. → 3. If none matches, say so — do not guess or crash. → 4. Otherwise, compute and print the result.

1

2

Ask: convert which way?

Choice 1 or 2?

Read Celsius

Read Fahrenheit

Multiply by 9/5, add 32

Subtract 32, multiply by 5/9

Show Fahrenheit

Show Celsius

3 The Build — explained part by part

Here is the complete converter. The interesting part is not the arithmetic (that is six one-line formulas); it is how the function reports “that choice does not exist” without a magic sentinel number or an exception for what is really just bad caller input.

C++TemperatureConverter.hpp / TemperatureConverter.cpp / main.cpp
#pragma once
#include <optional>
#include <string>

// Converts `value` according to `choice` (one of "CtoF", "FtoC", "CtoK",
// "KtoC", "FtoK", "KtoF"). Returns std::nullopt for an unrecognized choice
// instead of throwing or returning a sentinel number -- the modern C++
// answer to "this might not produce a value," and a good match for
// something that fails only on caller error, not on I/O or the environment.
std::optional<double> convert(const std::string &choice, double value);

#include "TemperatureConverter.hpp"

std::optional<double> convert(const std::string &choice, double value) {
    if (choice == "CtoF") return value * 9.0 / 5.0 + 32.0;
    if (choice == "FtoC") return (value - 32.0) * 5.0 / 9.0;
    if (choice == "CtoK") return value + 273.15;
    if (choice == "KtoC") return value - 273.15;
    if (choice == "FtoK") return (value - 32.0) * 5.0 / 9.0 + 273.15;
    if (choice == "KtoF") return (value - 273.15) * 9.0 / 5.0 + 32.0;
    return std::nullopt;
}

#include "TemperatureConverter.hpp"
#include <iostream>
#include <string>

int main() {
    std::cout << "Choice (CtoF, FtoC, CtoK, KtoC, FtoK, KtoF): ";
    std::string choice;
    std::cin >> choice;
    std::cout << "Value: ";
    double value;
    std::cin >> value;

    auto result = convert(choice, value);
    if (result) {
        std::cout << value << " (" << choice << ") = " << *result << "\n";
    } else {
        std::cout << "Unknown conversion: " << choice << "\n";
    }
    return 0;
}
⚠ No in-browser playground here
C++ compiles to a real, native binary, so unlike the Python version of this project there is no editor above you can run in the browser. Copy the code below and run it on your own machine — it takes seconds once a C++17-or-newer compiler like g++ or clang++ is installed.
What each part does — in plain words
std::optional<double> convert(...) — C has no nullable numeric type at all (the basic-project version used an out-parameter plus a bool), and older C++ might have used a sentinel like NAN or thrown an exception. std::optional is the modern, type-safe middle ground: the return type itself says “this might not have a value,” and the compiler will not let you use *result without checking result.has_value() first (well, it will let you, but if (result) is the idiom every C++ programmer reaches for).

if (choice == "CtoF") return value * 9.0 / 5.0 + 32.0; if (choice == "FtoC") return ... — a plain chain of string comparisons, deliberately simple; a larger project with many more cases might reach for a std::unordered_map<std::string, std::function<double(double)>> instead, trading a little setup complexity for an O(1) lookup.

return std::nullopt; — the empty state of an optional, returned when no choice matched. The caller checks it with if (result) in main.
Common mistakes — and how to avoid them
✗ Dereferencing an std::optional with *result before checking it has a value — this is undefined behaviour, not a clean crash, if the optional is empty.
✓ Always check with if (result) or result.has_value() first, as main does here, or use result.value(), which at least throws a catchable exception instead.
✗ Comparing floating-point results with == in a test — 0.1 + 0.2 is not exactly 0.3 in binary floating point, so an exact-equality test can fail for a mathematically correct answer.
✓ Compare within a small tolerance instead, as this project’s close() test helper does with std::fabs(a - b) < 0.001.

4 Test & Prove Each Part

We test each conversion direction plus the unknown-choice case, using a small tolerance helper for the floating-point comparisons.

Celsius to Fahrenheit is correct
Fahrenheit to Celsius is correct
Celsius to Kelvin is correct
Kelvin to Fahrenheit is correct
An unknown choice returns std::nullopt, not a crash or a garbage number
C++test_TemperatureConverter.cpp
#include "TemperatureConverter.hpp"
#include <cassert>
#include <cmath>
#include <iostream>

#define RUN(name) do { name(); std::cout << "PASS: " << #name << "\n"; } while (0)

static bool close(double a, double b) { return std::fabs(a - b) < 0.001; }

static void converts_celsius_to_fahrenheit() {
    auto r = convert("CtoF", 100.0);
    assert(r.has_value());
    assert(close(*r, 212.0));
}

static void converts_fahrenheit_to_celsius() {
    auto r = convert("FtoC", 32.0);
    assert(r.has_value());
    assert(close(*r, 0.0));
}

static void converts_celsius_to_kelvin() {
    auto r = convert("CtoK", 0.0);
    assert(r.has_value());
    assert(close(*r, 273.15));
}

static void converts_kelvin_to_fahrenheit() {
    auto r = convert("KtoF", 273.15);
    assert(r.has_value());
    assert(close(*r, 32.0));
}

static void an_unknown_choice_returns_nullopt_not_a_crash() {
    auto r = convert("XtoY", 100.0);
    assert(!r.has_value());
}

int main() {
    RUN(converts_celsius_to_fahrenheit);
    RUN(converts_fahrenheit_to_celsius);
    RUN(converts_celsius_to_kelvin);
    RUN(converts_kelvin_to_fahrenheit);
    RUN(an_unknown_choice_returns_nullopt_not_a_crash);
    std::cout << "All tests passed.\n";
    return 0;
}

Compile and run with g++ -std=c++20 -o test_run TemperatureConverter.cpp test_TemperatureConverter.cpp && ./test_run.

5 The Interface

INPUTINPUTa choice code and a value
What it expects
CtoF
100
OUTPUTOUTPUTthe converted value, or an error message
What it returns
100 (CtoF) = 212

6 Run It & Automate It

Save the code as TemperatureConverter.hpp / TemperatureConverter.cpp / main.cpp and compile it with g++ — that turns your source directly into a native executable for your machine. No separate runtime needed: the compiled binary runs on its own.

Run it locally
g++ -std=c++20 -o convert main.cpp TemperatureConverter.cpp && ./convert
Prompts for a choice code, then a value.

A CI tool like Jenkins runs the same compile-then-test-then-check-for-leaks steps automatically whenever the code changes — every line below has a plain explanation.

What you should see when it works
Terminala real run
$ ./convert
Choice (CtoF, FtoC, CtoK, KtoC, FtoK, KtoF): CtoF
Value: 100
100 (CtoF) = 212
If it breaks — how to fix it
🚨 Unknown conversion: ctof
Choice codes are case-sensitive — the comparisons use exact std::string equality, so ctof does not match CtoF.
GroovyJenkinsfile
// Jenkinsfile — compiles, tests, and checks for leaks on every change.
pipeline {
    agent any

    stages {
        stage('Get the code') {
            // download the latest code
            steps { checkout scm }
        }
        stage('Compile') {
            steps {
                // confirm a compiler is installed
                sh 'g++ --version'
                // compile with strict warnings on
                sh 'g++ -std=c++20 -Wall -Wextra -o app *.cpp'
            }
        }
        stage('Run the tests') {
            steps {
                // prints PASS/FAIL, exits non-zero on failure
                sh './app'
            }
        }
        stage('Check for memory leaks') {
            steps {
                // fails the build on any leak or invalid access
                sh 'valgrind --error-exitcode=1 --leak-check=full ./app'
            }
        }
    }

    post {
        success { echo 'All tests passed, no leaks found.' }
        failure { echo 'A test or Valgrind check failed — see above.' }
    }
}
🎯 Try this next — make it yours
  1. Make the choice case-insensitive. Normalize the input before comparing. (Teaches: string transformation with std::transform and std::tolower.)
  2. Add Rankine. A seventh scale, a seventh formula. (Teaches: extending a chain of conditions cleanly.)
  3. Use a lookup table instead of if-chains. Map each choice string to a std::function<double(double)>. (Teaches: storing functions as values.)
What you learned
You learned to model “this might not produce a value” with std::optional instead of a sentinel number, an out-parameter, or an exception, and why comparing floating-point results needs a tolerance, not ==. Related: Modern C++ (C++11–C++23), Exceptions.