Lesson overview
main is special because the runtime calls it to start an executable. It is not an ordinary helper function, and a library should not define it unless that library is being built as an executable target.
Learning objectives
- Identify valid portable
mainsignatures. - Explain how
argcandargvrepresent command-line arguments. - Return meaningful exit codes for success, usage errors and parse failures.
- Refactor logic out of
maininto testable helper functions. - Distinguish executable targets from library targets.
Key vocabulary before you start
Entry point
The function where execution of a C++ executable begins.
Portable signature
A main form accepted by the C++ standard across conforming implementations.
Command-line argument
A string supplied to the program by the host environment.
Exit code
The integer status returned to the host process.
Executable boundary
The layer that turns user input into calls to testable library code.
Parse failure
A case where an input string cannot safely become the expected value.
Entry-point map
main is the boundary between the operating system, the C++ runtime and your program.
One executable, one main
Only the executable target should define main.
Parse arguments deliberately
argv[0] is the program name; user arguments begin at argv[1].
Return status
Exit codes communicate success or failure to the caller.
Keep main thin
Put testable behaviour in functions and let main orchestrate.
Example: valid main with arguments
The common command-line signature receives a count and an array of C strings.
#include <iostream>
int main(int argc, char* argv[]) {
if (argc > 1) {
std::cout << "First argument: " << argv[1] << '\n';
}
return 0;
}Trace the state
- The runtime starts the executable and calls
main. argccontains the number of argument strings.argv[0]names the program;argv[1]is the first user-supplied argument.- Returning
0reports success to the operating system.
| Line or operation | Variable | Value | State role | Invariant or check |
|---|---|---|---|---|
| Read the example before running it | Key names and calls | Known from source | Reasoning setup | Each named element should support the lesson focus: The main Function in C++. |
| Trace the first meaningful operation | Primary state or boundary | Established by initialisation, call or condition | Valid-state checkpoint | Later reasoning must not use the value before this checkpoint. |
| Identify the first decision or dereference | Control or access point | Depends on current state | Failure-mode checkpoint | The condition, pointer, argument or dependency must be valid before use. |
| Record the observable result | Return value, output or mutation | Produced by the example | Evidence checkpoint | The result should match the contract explained in the lesson. |
Visual model: executable boundary around library code
Keep main small enough that parsing, exit codes and library calls can each be reasoned about.
Start processThe host starts the executable and provides argc and argv.
Validate argumentsmain checks count and syntax before trusting user input.
Parse into domain valuesStrings become typed values or produce explicit parse errors.
Call library codeThe real computation lives in testable functions outside main.
Return statusmain reports success, usage error or parse failure with an exit code.
Boundary task
Mark which lines in your solution belong to the executable boundary and which should move into library functions.
Why main exists
Every executable needs a single place where control enters user code. In C++, that place is main.
The runtime performs startup work before main, and shutdown work after it returns. That is why main should normally return rather than forcing termination.
Do not call main yourself. If you need reusable program logic, put it in a run function or another helper and call that from main.
Thin main
Move testable behaviour out of the special entry point.
int run(int argc, char* argv[]) {
// parse, compute, report
return 0;
}
int main(int argc, char* argv[]) {
return run(argc, argv);
}Applied task
Explain why calling run is acceptable but recursively calling main is the wrong abstraction.
Valid signatures and arguments
Portable C++ uses int main() or int main(int argc, char* argv[]). The second form exposes command-line arguments.
argc is the number of argument strings. argv is an array of pointers to those strings. The program name is conventionally at argv[0].
Arguments containing spaces are handled by the shell before main is called. A quoted argument such as "New York" arrives as one argv element.
Argument loop
User arguments start at index 1.
for (int i = 1; i < argc; ++i) {
std::cout << i << ": " << argv[i] << '\n';
}Applied task
Given a command line with quoted arguments, list argc and each argv element.
Return values and exit codes
The integer returned by main is the program's status code. By convention, 0 means success and non-zero values describe failure.
Meaningful exit codes make scripts, tests and build systems easier to diagnose.
return from main allows normal local cleanup. std::exit is a sharper tool and changes destructor behaviour for local automatic objects.
Named exit codes
Names make failure modes easier to review.
enum class ExitCode : int {
Ok = 0,
BadUsage = 64,
ParseError = 65
};
int main(int argc, char* argv[]) {
if (argc < 2) return static_cast<int>(ExitCode::BadUsage);
return static_cast<int>(ExitCode::Ok);
}Applied task
Choose exit codes for success, missing input and malformed input.
Parse command-line input safely
Do not assume command-line strings are valid just because they exist. Convert them and handle failure.
std::from_chars is useful for numeric parsing because it does not allocate and reports exactly where parsing stopped.
Parsing belongs near the program boundary. Once parsed, pass typed values into ordinary functions.
Parse an integer
A parse function makes success and failure explicit.
#include <charconv>
#include <optional>
#include <string_view>
std::optional<int> parse_int(std::string_view text) {
int value{};
auto [ptr, ec] = std::from_chars(text.data(), text.data() + text.size(), value);
if (ec != std::errc{} || ptr != text.data() + text.size()) return std::nullopt;
return value;
}Applied task
Parse three arguments as integers and report the first failure without crashing.
Executable vs library targets
Only the executable needs main. A library should expose declarations and definitions that executable code can call.
If a library defines main, it stops being reusable in other executables and causes link conflicts when combined with another main.
A clean layout separates lib.hpp, lib.cpp and main.cpp so the library can be tested independently.
Library and executable layout
The library provides sum; the executable owns main.
// lib.hpp
#pragma once
#include <vector>
int sum(const std::vector<int>& values);
// lib.cpp
#include "lib.hpp"
int sum(const std::vector<int>& values) {
int total = 0;
for (int v : values) total += v;
return total;
}
// main.cpp
#include <iostream>
#include "lib.hpp"
int main(int argc, char* argv[]) {
// parse argv[1..] to ints, call sum, print result
return 0;
}Applied task
Explain why a unit-test executable and a production executable can both link the same library, but each needs its own main.
Applied case lab
Case 1: The extra main
A build target includes two demonstration files, each with main. Diagnose the link error and propose a target split.
Case 2: The missing argument
A program reads argv[1] without checking argc. Explain the failure mode and write the guard.
Case 3: The untestable main
All logic lives inside main. Extract parsing, computation and output so core logic can be tested.
Applied task: mastering main
Practise valid signatures, argument indexing, safe parsing and executable/library separation.
Stage 1: read and classify
A. Validate signatures
Decide which forms are portable entry points.
int main();
int main(int argc, char* argv[]);
void main();
int main(int argc, char** argv);- Mark the portable signatures.
- Explain why
void mainshould not be used.
Reveal one possible refactor
int main() { return 0; }
int main(int argc, char* argv[]) {
return argc > 0 ? 0 : 1;
}Model reasoning
A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.
Stage 2: trace or repair
B. Fix an off-by-one argument loop
This loop wrongly treats the program name as user input.
for (int i = 0; i < argc; ++i) {
parse(argv[i]);
}- Start at the first user argument.
- Guard the no-argument case.
Reveal one possible refactor
if (argc < 2) return static_cast<int>(ExitCode::BadUsage);
for (int i = 1; i < argc; ++i) {
parse(argv[i]);
}Model reasoning
A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.
Stage 3: justify the design
C. App vs library
Finish the executable without putting main in the library.
// main.cpp
#include <iostream>
#include "lib.hpp"
int main(int argc, char* argv[]) {
// TODO: parse argv[1..] to ints, call sum, print result
}- Explain why only the executable needs
main. - Keep
sumin the library and parsing in the executable boundary.
Reveal one possible refactor
int main(int argc, char* argv[]) {
std::vector<int> values;
for (int i = 1; i < argc; ++i) {
auto parsed = parse_int(argv[i]);
if (!parsed) return static_cast<int>(ExitCode::ParseError);
values.push_back(*parsed);
}
std::cout << sum(values) << '\n';
return static_cast<int>(ExitCode::Ok);
}Model reasoning
A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.
Quick checks
1. Which argv index is normally the first user-supplied argument?
2. Why should a library not define main?
3. What does returning 0 from main conventionally mean?
AI-augmented practice notes
Use these as prompts for disciplined support, not as permission to outsource judgement.
Entry Point Coach
- Ask AI to validate your
mainsignature and portability. - Ask for an argument parser that converts
argvto typed values withfrom_chars. - Ask AI to propose a small enum of exit codes and where to return them.
- Ask for a comparison of
returnfrommainandstd::exitfor destructors andatexithandlers.
Assessment tasks
- Build a tiny command-line program that parses integer arguments and prints their sum.
- Document the chosen exit codes and give one example command for each.
- Split the program into
lib.hpp,lib.cppandmain.cpp.
Judgement questions
What should stay in main, and what should move out?
Separate boundary work such as parsing and status reporting from core logic that should be testable.
How does your shell affect argv?
Explain how quoted arguments with spaces are delivered to main.
