SovranCode
Modern C++ Lesson 47
Course contents50 available · 0 planned

Get a program running

01 · C++ Introduction: What Is C++?02 · How to Install a C++ Compiler and Set Up Your Editor03 · C++ Hello World: Your First Program Explained04 · How to Compile and Run C++ Programs05 · C++ Debugging: Warnings and Breakpoints

Work with values and input

06 · C++ Variables: Initialization and Scope07 · C++ Data Types: int, double and Type Conversion08 · C++ const, constexpr and auto Explained09 · C++ Operators: Arithmetic, Comparison and Logic10 · C++ User Input: cin, getline and Validation

Control flow and functions

11 · C++ Conditions: if, else if and else12 · C++ Switch Statement: Cases, break and default13 · C++ For Loop: Syntax and Examples14 · C++ While and Do-While Loops: Examples15 · C++ Functions: Parameters and Return Values

Strings and sequences

16 · C++ Strings: std::string with Examples17 · C++ Vector: std::vector with Examples18 · C++ Arrays: std::array and Bounds Checking19 · C++ Range-Based For Loop and Iterators20 · C++ Vector Practice: Build a Data Summary

Model data with classes

21 · C++ Structs and enum class Explained22 · C++ Classes and Objects: Encapsulation23 · C++ Constructors and Member Initialization24 · C++ Const Member Functions and Composition25 · C++ Copying Objects and the Rule of Zero

Lifetime and resource ownership

26 · C++ Object Lifetime, Destructors and RAII27 · C++ Pointers and References: nullptr and Borrowing28 · C++ unique_ptr: Smart Pointers and Ownership29 · C++ Move Semantics: std::move vs Copying30 · C++ string_view and span: Avoid Dangling Views

Containers and algorithms

31 · C++ map vs unordered_map: Lookup and Examples32 · C++ set, queue and stack: When to Use Each33 · C++ Lambda Expressions and STL Algorithms34 · C++ sort and binary_search: Rules and Complexity35 · C++20 Ranges: Views, filter and transform

Errors, files, and interfaces

36 · C++ optional and variant Explained37 · C++ Exceptions: try, catch and Safe Cleanup38 · C++ File Handling: Read and Write Text Files39 · C++ Input Parsing and Data Validation40 · C++ Polymorphism: Virtual Functions and Interfaces

Generic programming and quality

41 · C++ Templates: Function and Class Examples42 · C++20 Concepts and Template Constraints43 · C++ Unit Testing: Normal and Boundary Cases44 · C++ Sanitizers: Find Memory and Undefined Behavior Bugs45 · C++ Performance: Benchmark Before Optimizing

Build and deliver a complete application

46 · C++ Header Files and Translation Units47 · C++ CMake Tutorial: Build an Executable48 · C++ Dependencies and CTest: Automated Tests49 · C++ Multithreading: Threads and Data Races50 · C++ Command-Line Applications: Help and Release Checks
Modern C++50 available · 0 planned

Get a program running

01 · C++ Introduction: What Is C++?02 · How to Install a C++ Compiler and Set Up Your Editor03 · C++ Hello World: Your First Program Explained04 · How to Compile and Run C++ Programs05 · C++ Debugging: Warnings and Breakpoints

Work with values and input

06 · C++ Variables: Initialization and Scope07 · C++ Data Types: int, double and Type Conversion08 · C++ const, constexpr and auto Explained09 · C++ Operators: Arithmetic, Comparison and Logic10 · C++ User Input: cin, getline and Validation

Control flow and functions

11 · C++ Conditions: if, else if and else12 · C++ Switch Statement: Cases, break and default13 · C++ For Loop: Syntax and Examples14 · C++ While and Do-While Loops: Examples15 · C++ Functions: Parameters and Return Values

Strings and sequences

16 · C++ Strings: std::string with Examples17 · C++ Vector: std::vector with Examples18 · C++ Arrays: std::array and Bounds Checking19 · C++ Range-Based For Loop and Iterators20 · C++ Vector Practice: Build a Data Summary

Model data with classes

21 · C++ Structs and enum class Explained22 · C++ Classes and Objects: Encapsulation23 · C++ Constructors and Member Initialization24 · C++ Const Member Functions and Composition25 · C++ Copying Objects and the Rule of Zero

Lifetime and resource ownership

26 · C++ Object Lifetime, Destructors and RAII27 · C++ Pointers and References: nullptr and Borrowing28 · C++ unique_ptr: Smart Pointers and Ownership29 · C++ Move Semantics: std::move vs Copying30 · C++ string_view and span: Avoid Dangling Views

Containers and algorithms

31 · C++ map vs unordered_map: Lookup and Examples32 · C++ set, queue and stack: When to Use Each33 · C++ Lambda Expressions and STL Algorithms34 · C++ sort and binary_search: Rules and Complexity35 · C++20 Ranges: Views, filter and transform

Errors, files, and interfaces

36 · C++ optional and variant Explained37 · C++ Exceptions: try, catch and Safe Cleanup38 · C++ File Handling: Read and Write Text Files39 · C++ Input Parsing and Data Validation40 · C++ Polymorphism: Virtual Functions and Interfaces

Generic programming and quality

41 · C++ Templates: Function and Class Examples42 · C++20 Concepts and Template Constraints43 · C++ Unit Testing: Normal and Boundary Cases44 · C++ Sanitizers: Find Memory and Undefined Behavior Bugs45 · C++ Performance: Benchmark Before Optimizing

Build and deliver a complete application

46 · C++ Header Files and Translation Units47 · C++ CMake Tutorial: Build an Executable48 · C++ Dependencies and CTest: Automated Tests49 · C++ Multithreading: Threads and Data Races50 · C++ Command-Line Applications: Help and Release Checks
PREVIOUS LESSONC++ Header Files and Translation Units
NEXT LESSONC++ Dependencies and CTest: Automated Tests
C++ foundations · Lesson 47 About 52 min with practice

C++ CMake Tutorial: Build an Executable

Learn CMake for C++ step by step: create an executable target, organize source and header files, request C++20, and configure and build outside your source folder.

Before you begin

Understand headers and translation units. Install CMake 3.20 or newer and a C++20-capable compiler for the local project. CMake is not included with every editor or compiler installation.

What you will learn

  • Explain the difference between configure, build and run.
  • List the source files belonging to an executable target.
  • Attach include paths and language requirements to a target.
  • Keep generated files in a separate build directory.
  • Rebuild a changed program and verify the intended executable.

CMake describes how a project should be built

In the previous lesson you compiled several files by naming them in a terminal command. That is a good way to understand compilation and linking. As a project grows, repeating long commands becomes easy to get wrong. One person may forget a source file; another may use a different language option. CMake records the build structure in text so that a build tool can follow the same project instructions on another machine.

CMake is not your C++ compiler. It reads project instructions, checks the selected environment and generates a build system for a tool such as Make, Ninja or a supported IDE. The compiler still translates C++ and the linker still connects definitions. This distinction matters when diagnosing failures. A missing compiler is an environment problem. A missing semicolon is a source problem. Installing another editor does not automatically repair either one.

We will build a small report program from three files. The header declares boundedTotal. Its source file defines the rule, accepting operands from minus one hundred through one hundred. The main file reads two whole integer lines and reports their sum. A present zero is a valid answer. Keeping the domain rule unchanged lets you concentrate on how the same behavior becomes a named build target rather than learning a new algorithm at the same time.

Source files main.cpp and total.cpp and the total.hpp header belong to a report target. Generated files live in a separate build directory; the selected compiler and linker produce the report executable.
A target groups build requirements. CMake configures the build system; your toolchain compiles and links the files. Keeping build output separate makes the source folder easier to inspect and share.

Make the folder layout explicit

Create one project folder containing CMakeLists.txt, an include folder and a src folder. Save total.hpp inside include. Save main.cpp and total.cpp inside src. The paths in the build description are relative to the directory containing CMakeLists.txt. A misspelled folder name is enough to prevent configuration. Check actual filenames, including extensions and letter case, before changing otherwise valid C++ code.

Do not place total.cpp inside the header with an include directive. CMake lists total.cpp as a translation unit to compile once. The source files include total.hpp to see the declaration. The include path tells the compiler where to look for that header without hard-coding an absolute path from your computer. This is why a project can move to a different user folder while keeping its own layout.

The following blocks are project files, not separate programs. Save all four before building. A header alone does not define main, and the main file needs a definition from another source file at link time. Below the project you will also find complete single-file versions for the in-page editor. Those teach the same rule but do not pretend that the page editor runs CMake or manages several files for you.

PROJECT FILE · LOCAL BUILD

include/total.hpp

include/total.hpp

Save this file at the shown path inside the project folder. Build the complete project locally; this file is not an independent in-page program.

#ifndef SOVRA_TOTAL_HPP
#define SOVRA_TOTAL_HPP
#include <optional>
std::optional<int> boundedTotal(int first, int second);
#endif
PROJECT FILE · LOCAL BUILD

src/total.cpp

src/total.cpp

Save this file at the shown path inside the project folder. Build the complete project locally; this file is not an independent in-page program.

#include "total.hpp"
std::optional<int> boundedTotal(int first, int second) {
    if (first < -100 || first > 100 || second < -100 || second > 100)
        return std::nullopt;
    return first + second;
}
PROJECT FILE · LOCAL BUILD

src/main.cpp

src/main.cpp

Save this file at the shown path inside the project folder. Build the complete project locally; this file is not an independent in-page program.

#include "total.hpp"
#include <iostream>
#include <sstream>
#include <string>
bool readInteger(int& value) {
    std::string line;
    if (!std::getline(std::cin, line)) return false;
    std::istringstream parser{line};
    if (!(parser >> value)) return false;
    parser >> std::ws;
    return parser.eof();
}
int main() {
    int first{}, second{};
    if (!readInteger(first) || !readInteger(second)) {
        std::cout << "Invalid input\n"; return 1;
    }
    const auto total = boundedTotal(first, second);
    if (!total) { std::cout << "Rejected\n"; return 2; }
    std::cout << "Total: " << *total << '\n';
}
PROJECT FILE · LOCAL BUILD

CMakeLists.txt

CMakeLists.txt

Save this file at the shown path inside the project folder. Build the complete project locally; this file is not an independent in-page program.

cmake_minimum_required(VERSION 3.20)
project(SovraReport LANGUAGES CXX)
add_executable(report src/main.cpp src/total.cpp)
target_include_directories(report PRIVATE include)
target_compile_features(report PRIVATE cxx_std_20)
set_target_properties(report PROPERTIES CXX_EXTENSIONS OFF)

Read the build description one decision at a time

The minimum-version line sets the oldest supported CMake version for these instructions. The project line gives the project a name and enables C++. add_executable creates a target named report and names its source files. A target is the build system's named unit of work. The name is not a variable storing the program's runtime output; it identifies what should be produced and what requirements belong to it.

target_include_directories attaches the include folder to report. PRIVATE means that this requirement belongs to this target's own compilation. target_compile_features requests at least the C++20 feature level using cxx_std_20. It does not install a newer compiler or guarantee every library feature on every platform. Disabling compiler extensions makes the target request a standard language mode rather than a vendor-extended one where the toolchain supports that distinction.

  • Create report before attaching settings to it.
  • List src/main.cpp and src/total.cpp as sources; do not compile a copied second definition.
  • Attach the include directory to report instead of changing global include settings.
  • Request C++20 and keep the generated build directory separate from source.
LOCAL TERMINAL COMMANDS

Configure, build, then run

Run from the project root after saving all files. Use only the run path matching your platform and generator. Stop if configure or build reports an error.

cmake --version
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build --config Debug

# macOS / Linux, typical single-configuration generator:
./build/report
# Enter 3, then 4 on separate lines: Total: 7

# Windows, typical Visual Studio multi-configuration generator:
.\build\Debug\report.exe
# Windows Ninja/Make layouts usually use .\build\report.exe instead.

Configure is not the same as build or run

The configure command reads the project and writes generated state into build. The -S option chooses the source directory and -B chooses the build directory. The next command asks the generated build system to compile and link. Only after that succeeds should you launch the executable. Seeing a successful configuration message does not mean that every source file compiled. Seeing a successful build does not mean that you tested the program's input behavior.

Some generators keep one configuration in a build directory. For these, CMAKE_BUILD_TYPE chooses Debug here. Other generators, such as Visual Studio, keep several configurations together; --config Debug selects which one to build. That is why the example passes both forms and shows more than one possible executable path. Inspect the generator and build output rather than assuming every operating system places an executable in exactly the same directory.

When you change only C++ code, run the build command again. The build tool normally recompiles affected work. When you change CMakeLists.txt, the generated build system commonly requests reconfiguration as part of the next build, but an explicit configure command is also clear. When changing compiler or generator, use a new build directory such as build-clang instead of mixing incompatible cached choices in an existing one.

C++ PRACTICE RUNNER

Practise the pure rule without project files

This is a complete single-file counterpart, not a CMake execution. Notice that a valid zero remains a present optional value.

#include <iostream>
#include <optional>
std::optional<int> boundedTotal(int first, int second) {
    if (first < -100 || first > 100 || second < -100 || second > 100)
        return std::nullopt;
    return first + second;
}
int main() { std::cout << "Total: " << *boundedTotal(3, 4) << '\n'; }
TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

Compare with the starter's expected output
Total: 7

After editing inputs, calculate a new prediction; this reference describes only the starter.

C++ PRACTICE RUNNER

Check an exact endpoint and zero

The conditional tests presence, not whether the stored sum is nonzero.

#include <iostream>
#include <optional>
std::optional<int> boundedTotal(int first, int second) {
    if (first < -100 || first > 100 || second < -100 || second > 100)
        return std::nullopt;
    return first + second;
}
int main() {
    for (const auto value : {boundedTotal(-100, 100), boundedTotal(101, 0)}) {
        if (value) std::cout << "Total: " << *value << '\n';
        else std::cout << "Rejected\n";
    }
}
TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

Compare with the starter's expected output
Total: 0
Rejected

After editing inputs, calculate a new prediction; this reference describes only the starter.

C++ PRACTICE RUNNER

Run the complete input path

Enter one integer per line. Syntax failure and domain rejection are deliberately different outcomes.

#include <iostream>
#include <sstream>
#include <string>
bool readInteger(int& value) {
    std::string line;
    if (!std::getline(std::cin, line)) return false;
    std::istringstream parser{line};
    if (!(parser >> value)) return false;
    parser >> std::ws;
    return parser.eof();
}
#include <optional>
std::optional<int> boundedTotal(int first, int second) {
    if (first < -100 || first > 100 || second < -100 || second > 100)
        return std::nullopt;
    return first + second;
}
int main() {
    int first{}, second{};
    if (!readInteger(first) || !readInteger(second)) {
        std::cout << "Invalid input\n"; return 1;
    }
    const auto total = boundedTotal(first, second);
    if (!total) { std::cout << "Rejected\n"; return 2; }
    std::cout << "Total: " << *total << '\n';
}

TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

Compare with the starter's expected output
Total: 7

After editing inputs, calculate a new prediction; this reference describes only the starter.

Prove the project works without old output

Before calling the setup repeatable, configure into a new empty directory such as build-check and build there. This avoids accidentally relying on an old object file or a locally edited cache. Run a normal example, a zero result, a boundary result and an invalid input. Write down the compiler version, CMake version, generator and commands. Another student should be able to follow those instructions without guessing your editor settings.

Keep source files and build instructions in version control. Generated object files, caches and machine-specific executable output usually belong in ignored build folders. Do not delete a folder merely because its name contains build: first verify that it is generated output and contains no unique work. A fresh build in a new folder is enough for this lesson and does not require any destructive cleanup command.

For the lab, first run the single-file behavior below, then recreate it using the listed project files. Change one accepted input and predict the answer before running. Finally change the implementation to subtraction temporarily and observe a different result after rebuilding. Restore addition and rebuild again. This small check proves that you are launching the executable produced from your current saved source, not a stale file from another folder.

APPLY THE CONCEPT

Independent lab

Verify bounded addition in the editor, then package the identical behavior as the CMake report target.

Acceptance checklist

  • Read two complete integer lines, rejecting malformed or missing input with Invalid input and status 1.
  • Accept each operand only in -100..100; print Rejected and return 2 otherwise.
  • Print Total followed by the valid sum, preserving a present zero.
  • Create the four listed project files and attach sources, includes and C++20 to report.
  • Configure and build in a fresh directory; record versions and normal, zero, boundary and invalid transcripts.
Need a hint?
  1. The editor lab is one file; the real CMake project uses the separately shown files.
  2. An optional containing zero still passes a presence check.
  3. Build successfully before running the new executable from the matching configuration.
C++ PRACTICE RUNNER

Write and run your lab

Build your own solution before revealing the reference. Use the input field for normal, boundary and invalid cases; this runner executes your program but does not grade it.

#include <iostream>
#include <string>
#include <sstream>
#include <limits>

int main() {
    // Implement the acceptance checklist above.
    return 0;
}

TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

Your lab evidence and notes

Record predictions, actual output, commands and the cause of each repair. Code is plain text: this scratchpad does not execute or grade C++.

Loading local notes…

Review your acceptance checklist and quiz explanations before marking this lesson complete. This is self-review, not a grade or certification.

Reveal the explained lab solution
C++ PRACTICE RUNNER

Reference lab solution

Compare the reference with your own reasoning. Running this solution does not grade or complete the lesson.

#include <iostream>
#include <sstream>
#include <string>
bool readInteger(int& value) {
    std::string line;
    if (!std::getline(std::cin, line)) return false;
    std::istringstream parser{line};
    if (!(parser >> value)) return false;
    parser >> std::ws;
    return parser.eof();
}
#include <optional>
std::optional<int> boundedTotal(int first, int second) {
    if (first < -100 || first > 100 || second < -100 || second > 100)
        return std::nullopt;
    return first + second;
}
int main() {
    int first{}, second{};
    if (!readInteger(first) || !readInteger(second)) {
        std::cout << "Invalid input\n"; return 1;
    }
    const auto total = boundedTotal(first, second);
    if (!total) { std::cout << "Rejected\n"; return 2; }
    std::cout << "Total: " << *total << '\n';
}

TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

Expected output

Total: 7

The whole-line parser rejects trailing junk instead of silently accepting a numeric prefix.

The domain rule bounds both operands before addition, so the small sum fits int and zero is not confused with rejection.

The CMake target changes how the files are built, not what valid inputs mean. A fresh-directory build checks that the saved project supplies its own requirements.

Review before moving on

Explain the lesson's objectives in your own words, compare your lab evidence with the acceptance checklist, and keep the first failing case alongside your repair. Completing a page is not proof that every C++ feature or input is supported.

  • Explain the difference between configure, build and run.
  • List the source files belonging to an executable target.
  • Attach include paths and language requirements to a target.
  • Keep generated files in a separate build directory.
  • Rebuild a changed program and verify the intended executable.

References and further reading

Original examples and exercises accompany these primary references. Tool-specific diagnostics and support depend on your installed version.

  • CMake: target_compile_features
  • CMake: build-system targets
KNOWLEDGE CHECK

Check your understanding

Answer all five questions, then review the explanations. Answers are not saved or synced; completion is a separate self-review choice.

01What does cmake -S . -B build do?
02Why use target_include_directories with PRIVATE here?
03Does cxx_std_20 install missing library features?
04What should you do after a failed rebuild?
05How can you check for hidden dependence on cached output?
PREVIOUS LESSONC++ Header Files and Translation Units
NEXT LESSONC++ Dependencies and CTest: Automated Tests
ON THIS PAGELearning objectivesCMake describes how a project should be builtMake the folder layout explicitRead the build description one decision at a timeConfigure is not the same as build or runProve the project works without old outputIndependent labReviewKnowledge checkReferences
Course overview