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.
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.
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.
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.
#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;
}
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.
std::optional with *result before checking it has a value — this is undefined behaviour, not a clean crash, if the optional is empty.if (result) or result.has_value() first, as main does here, or use result.value(), which at least throws a catchable exception instead.== 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.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.
#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
What it expects
CtoF
100What it returns
100 (CtoF) = 2126 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.
g++ -std=c++20 -o convert main.cpp TemperatureConverter.cpp && ./convertPrompts 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.
$ ./convert
Choice (CtoF, FtoC, CtoK, KtoC, FtoK, KtoF): CtoF
Value: 100
100 (CtoF) = 212std::string equality, so ctof does not match CtoF.// 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.' }
}
}
- Make the choice case-insensitive. Normalize the input before comparing. (Teaches: string transformation with
std::transformandstd::tolower.) - Add Rankine. A seventh scale, a seventh formula. (Teaches: extending a chain of conditions cleanly.)
- Use a lookup table instead of if-chains. Map each choice string to a
std::function<double(double)>. (Teaches: storing functions as values.)
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.