Real input can be malformed, inaccessible or too large for your calculations. Treat those cases as part of the program's interface. A useful error tells the caller what failed and whether any output was produced. It should preserve enough context to fix the input without exposing unrelated internal details.
C++20 offers several error mechanisms. std::optional<T> represents a value that may be absent but carries no explanation. An error code can describe a recoverable failure without throwing. An exception transfers control to a matching handler and can carry a message or structured data. Choose a consistent policy at each boundary rather than discarding errors through unchecked defaults.
Parse first, then use the value
std::from_chars parses numbers without allocating or throwing for conversion failure. It returns an error code and a pointer to the first unconsumed character. Check both: successful parsing of 42extra does not mean the whole input was valid. Integer parsing does not skip leading whitespace or accept a leading plus sign.
Save as files.cpp. The input format is one decimal score from zero through one hundred per line, without surrounding whitespace. A terminal carriage return is removed to accept CRLF line endings too.
#include <charconv>
#include <cstddef>
#include <exception>
#include <fstream>
#include <iostream>
#include <limits>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <system_error>
std::optional<int> parse_score(std::string_view text) {
if (text.empty()) {
return std::nullopt;
}
int value{};
const auto result = std::from_chars(text.data(), text.data() + text.size(), value);
if (result.ec != std::errc{} || result.ptr != text.data() + text.size()
|| value < 0 || value > 100) {
return std::nullopt;
}
return value;
}
int main(int argc, char* argv[]) {
if (argc != 3) {
std::cerr << "Usage: files INPUT OUTPUT\n";
return 1;
}
try {
std::ifstream input{argv[1]};
if (!input) {
throw std::runtime_error("Cannot open input");
}
long long total{};
std::size_t line_number{};
std::string line;
while (std::getline(input, line)) {
if (line_number >= 10000) {
throw std::runtime_error("At most 10000 scores are supported");
}
++line_number;
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
const auto score = parse_score(line);
if (!score) {
throw std::runtime_error("Invalid score at line " + std::to_string(line_number));
}
if (total > std::numeric_limits<long long>::max() - *score) {
throw std::overflow_error("Total is out of range");
}
total += *score;
}
if (input.bad() || (input.fail() && !input.eof())) {
throw std::runtime_error("Reading input failed");
}
input.close();
std::ofstream output{argv[2]};
if (!output) {
throw std::runtime_error("Cannot open output");
}
output << "Total: " << total << '\n';
output.close();
if (!output) {
throw std::runtime_error("Writing output failed");
}
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
clang++ -std=c++20 -Wall -Wextra -Wpedantic -g files.cpp -o files
printf '70\n80\n90\n' > scores.txt
./files scores.txt summary.txt
cat summary.txt
The file contains Total: 240. Use a scratch directory: opening the output truncates an existing file. The program reads and validates the entire input before opening the output. An empty input produces a total of zero; an empty line is invalid.
Failure and cleanup are separate concerns
Streams normally record failures in state flags rather than throwing. A loop that stops reading might have reached ordinary end-of-file or encountered an I/O failure; inspect the state afterward. Closing the output explicitly lets this program check errors reported while flushing or closing. A destructor closes a file automatically but cannot provide this same caller-visible success check.
The exception handler catches by const reference, preserving the exception's dynamic type and avoiding a copy. As an exception propagates toward this handler, constructed local objects are destroyed. That handles resource cleanup, but it does not roll back external effects. A failed write can still leave a partial output file. Atomic replacement, crash durability and concurrent writers require additional filesystem design; they are not guarantees of a stream destructor.
The line-count limit bounds the total, and the explicit overflow guard also documents the addition's requirement. It does not bound individual line length: getline may allocate for a very long line. A program processing untrusted large files should add a bounded-reading policy.
Practice and solution guidance
- Test missing input, an invalid middle line and
80extra. Guidance: expect nonzero exit status; invalid input should not open or truncate the output. - Report an average. Guidance: retain the count, define the empty-file result and convert before division. Do not divide by zero.
- Return detailed parsing failures. Guidance: replace optional with a small result struct or
std::variant; distinguish format, range and trailing-character errors without relying on an absent value's contents.

