Introduction to Types in C/C++ and Algorithm Analysis
Igor Machado Coelho
13/09/2020 - 13/08/2026
This document was AI Translated to English. For reference, see the original one, in Portuguese.
The requirements for this class are knowledge of:
Understanding programming logic is the most important skill for a programmer! With it, you can easily switch programming languages, knowing only a few basic commands.
The first concept to be reviewed is that of a variable. A variable consists of a valid identifier (the same as for other popular languages such as Python) and stores some kind of data in the computer’s memory.
The C/C++ language is strongly typed, so the programmer must explicitly say which data type they wish to store in each variable, or let auto deduce it automatically.
int x = 5; // stores the integer 5 in variable x
char y = 'A'; // stores the character 'A' in variable y
float k = 3.7f; // stores the real number 3.7 in variable z
double z = 3.7; // stores the real number 3.7 in variable z
bool v = true; // stores the boolean true in variable v
auto b = 'B'; // type deduction with 'auto'... which type?
auto s = "abcd"; // character string, type still to be seenAnswer: What is the type of b above? (C++23 and C23)
Question/Answer: Be careful with types. What are the values stored in the variables below (C++23 and C23)?
int x1 = 5; // => 5
int x2 = x1 + 10; // => 15
int x3 = x2 / 2; // => 7
double x4 = x2 / 2; // => 7.0
double x5 = x2 / 2.0; // => 7.5
auto x6 = 15; // => 15
auto x7 = x2 / 2; // => 7
auto x8 = x2 / 2.0; // => 7.5Check these variable operations by writing to standard output (the computer screen).
Primitive types in C/C++ have a defined size, so it is good practice to use fixed sizes.
Prefer direct initialization with braces { }, rather
than indirect initialization by assignment (operator=).
Modularizing programs is very important, especially when code snippets are repeated many times.
In these cases, it is common to create routines, such as functions and procedures, which in turn may receive parameters.
Let us take as an example the function square, which returns the value passed raised to the square.
Important: the type deduction after the arrow
-> is done automatically.
In C, the printf command is typically used, but due to
countless security flaws, using a safer alternative is recommended.
Thus, in C++, to print to standard output we will use the
std::print command.
C++23 officially brings std::print and
std::println as part of the std standard
library module. To use it, you just need import std;.
import std;
int main() {
std::println("Hello World!");
return 0;
}
// online GCC: https://godbolt.org/z/j3W938PP6Question: What is the return value of the
main function? What does it mean?
To print to standard output we will use the std::print
command. Note: it is possible to avoid the std:: prefix
with a using namespace std;.
Question: how can we mix a text (also called a
character string, or string) with the content of variables?
Answer: through the substitution pattern
{}.
import std;
int main() {
int x1 = 7;
std::println("x1 is {}", x1); /* x1 is 7 */
double x6 = x1 / 2.0;
std::println("half of {} is {}", x1, x6); // half of 7 is 3.5
char b = 'L';
std::println("this is a {}etter", b); // this is a Letter
std::print("Hello world! \n"); // Hello world! (line break)
return 0;
}Problem: given x and y,
print the larger value.
Conditionals can be done through the if or if else commands.
int x = 15;
int y = 12;
if (x > y)
println("x is greater than y");
else
println("x is less than or equal to y");Question: What is the result of the expression
if(x = y)? And if(x == y)?
Loops can be done through while or for commands. A for command is divided into three parts: initialization, continuation condition and increment.
Question: What is printed in both loops?
void and std::monostate TypesWe have seen some types with a larger number of values, for example, int and char. int takes up 4 bytes and is able to support up to 232 distinct values (approx. -2 billion to +2 billion), while char supports up to 256 values, taking up only 1 byte.
In some cases, it is also useful to use types with a smaller number of values, such as std::monostate, which takes up 1 byte and has only a single value, and finally the void type, which is an incomplete type and does not represent any value. Since void does not represent values, it is not possible to create variables of type void!
int i = 10; // one out of 4 billion valid values
bool b = true; // one out of two valid values
// void v; // compilation error: void has no value
std::monostate m; // only one possible value
auto n = m; // monostate can be copiedImportant: the std::monostate type only
exists in C++, since in C there was never a need to define this type,
due to the lack of generic types (we will see this later).
When no value is returned (in a procedure), we use the void type. Procedures are useful even when no value is returned. Example: (from a to b):
Question: Why does the print_range
routine not need to return anything? What happens as a side
effect when calling print_range(2,5)?
Besides the primitive types presented earlier (int, float, char, …), the C/C++ language allows us to create composite types.
Task: study the remaining primitive types such as double and long long, as well as the modifiers unsigned, signed, short and long.
Composite types can be homogeneous aggregates (vectors/arrays) or heterogeneous aggregates (structs, …).
break and continueFlow control in loops can be done with break and
continue. break ends the execution of the loop
and continue restarts the loop.
Problem: Given an array B, find the first/last negative value, or print -1 if it does not exist.
goto (advanced topic)Unconditional jumps in the code can be done with
goto label; and label:. A usual application is
the “multiple break” of loops. Avoid using goto as much as
possible and, whenever possible, prefer structured alternatives such as
for, while, if,
else, break, etc.
Count how many prints are executed (variable z):
Examples will be given based on the GNU/Linux system and GCC
compilers, but there are equivalent tools for Windows and other
operating systems. The IDE Visual Studio Code supports the C++ language
both for Linux and for Windows, requiring CMake 4.0 with Ninja. On
Windows/WSL or Linux, install the Clang 19 compiler (on Windows, use
Scoop with scoop install main/llvm).
It is also possible to practice directly in a web browser with online platforms: onlinegdb.com/online_c++_compiler or Godbolt (more recommended!). In this case, the student can choose the compiler for C or for the C++ language (considering the C23 and C++23 standards).
To set up an IDE, read the tutorial “Breve Introdução ao C/C++ com IDE de Desenvolvimento” (in Portuguese).
The GCC compiler supports C/C++, so it can compile both C and C with C++.
Code main.c:
To compile manually (without CMake), run the following command with GCC 15:
g++ -std=c23 main.c -o example_c
Since C++ includes everything C has and still adds safer and simpler
features, such as import and print, we will
use the C++ standard in the compiler.
See the example at https://godbolt.org/z/j3W938PP6:
Code main.cpp:
To compile manually (without CMake), run the following command with GCC 15 or 16:
g++-15 -std=c++23 -fmodules -fsearch-include-path bits/std.cc main.cpp -o example
g++-16 -std=c++23 -fmodules --compile-std-module main.cpp -o example
For larger and more complex programs (with many files), it is necessary to use some build system, such as CMake or Bazel. On the next slide, an example of a CMakeLists.txt.
See the example at https://godbolt.org/z/We6fdojj1 (cmake 4.4.2):
CMakeLists.txt code (you need to install CMake 4.4 and Ninja)
cmake_minimum_required(VERSION 4.4)
# https://github.com/Kitware/CMake/blob/master/Help/dev/experimental.rst
set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "d0edc3af-4c50-42ea-a356-e2862fe7a444") # 4.1
# set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "451f2fe2-a8a2-47c3-bc32-94786d8fc91b") # 4.3
set(CMAKE_CXX_MODULE_STD 1)
project(my_project VERSION 0.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
add_executable(example src/main.cpp)
See the example at https://godbolt.org/z/We6fdojj1 (cmake 4.4.2):
To build, there are four commands:
mkdir -p build
cd build
cmake .. -GNinja
ninja
Another quite elegant solution is to use Bazel with the Clang compiler.
To use a development IDE such as VSCode, it is necessary to use Clang with the clangd extension for correct visual processing of the code (it does not work properly with GCC nor with Microsoft’s standard C/C++ extension).
See the example in the tutorial “Local
import std module on C++23 with Bazel”. You only need to configure
your MODULE.bazel and BUILD files (copy the extensions.bzl
from the tutorial!).
# MODULE.bazel
module(name = "project")
bazel_dep(name = "rules_cc", version = "0.2.17")
std_modules = use_extension("//:extensions.bzl", "local_libcxx_extension")
use_repo(std_modules, "std_modules")
# BUILD
load("@rules_cc//cc:defs.bzl", "cc_binary")
cc_binary(
name = "example",
srcs = ["main.cpp"],
deps = ["@std_modules"]
features = ["cpp_modules"]
)
See the example in the tutorial “Local import std module on C++23 with Bazel”.
To build and run, there are two commands:
bazel build ...
bazel run :example
Remember to update your .bazelrc file with the correct
compiler version:
# .bazelrc
build --repo_env=BAZEL_COMPILER=clang
build --repo_env=BAZEL_CXXOPTS=-stdlib=libc++
build --repo_env=BAZEL_LINKOPTS=-stdlib=libc++
build --repo_env=LIBCXX_MODULE_PATH=/usr/lib/llvm-21/share/libc++/v1
build --experimental_cpp_modules
build --cxxopt=-std=c++23
Charles Babbage (1864) says: > “As soon as analytic engine exists, it will necessarily guide the future course of science. Whenever any result is sought by its aid, the question will raise ? By what means of calculation can these results be arrived at by this machine in the shortest time?”
C/C++ comparison: remember to use struct or class public:, otherwise it will not be recognized as an aggregate type, but rather as an object, which works in a completely different way in the C++ language.
Important: we will always use struct throughout this C/C++ course as a trivial aggregate type, never as a class.
We return to the previous P struct example and ask ourselves, how do we access the internal variables of the aggregate P?
Just as in designated initialization, we can use the dot operator (.) to access fields of the aggregate. Example:
auto p1 = P{.y = 'A'};
p1.x = 20; // assigns 20 to variable x of p1
p1.x = p1.x + 1; // increments variable x of p1
println("{} {}", p1.x, p1.y); // prints '21 A'Example of struct p1, with p1.x and p1.y, from left to
right:
p1: | 21 | 'A' |
p1.x p1.y
Important: we will see later that (.) can also access internal methods of the aggregate type.
All variables of a program occupy a certain space in the computer’s main memory. We will assume that the int (or float) type takes up 4 bytes, while a char takes up only 1 byte.
In the case of arrays, the space occupied in memory is multiplied by the number of elements. Let us calculate the space of the variables:
int v[256]; // = 1024 bytes = 1 kibibyte = 1 KiB
char x[1000]; // = 1000 bytes = 1 kilobyte = 1 kB
float y[5]; // = 20 bytesIn aggregates, we assume the space occupied as the sum of its internal variables (although in practice the size may be slightly larger, due to memory alignment).
Important: in C++, trivial aggregates can also contain methods.
C++ allows the definition of generic types, that is, types that allow some other type to be passed as a parameter.
Let us consider the aggregate P that carries an int and a char… how do we transform it into a generic aggregate with respect to the variable x?
template<typename T>
struct G {
T x; // which type does variable x have?
char y;
};
// declares the generic aggregate G with type T=float or T=char
G<float> g1 = {.x = 3.14, .y = 'Y'};
G<char> g2 = {.x = 'A', .y = 'Y'};Question: How much space (in bytes) does each of these variables take up?
In C/C+, we can define a value as constant, through the word
const. A type change can be done with a type cast.
In C++, use static_cast<type> instead of the C-style
cast.
unsigned int x = 10; // 10
double y1 = x / -2; // 0
double y2 = (double)x / -2; // -5
double y3 = static_cast<double>(x) / -2; // -5 (C++)
const unsigned int z1 = x; // 10
// z1 = 20; // ERRORconst can be removed through a const_cast,
which is unsafe.
In C23 and C++23 there is constexpr, which unlike
const, can never be removed or redefined (unlike macros),
since it is resolved at compile time.
std::string and std::string_view Types
in the STLThe std::string type represents character strings,
called strings. It replaces the need for char*,
char[] or const char* in C.
If you need a lightweight “view” of a string, such as a substring,
use std::string_view (it avoids the full copy of the
string).
std::string s1 = "abcd";
std::string s2 = "ef";
println("length1={} length2={}", s1.length(), s2.length());
// length1=4 length2=2
s1 = s1 + s2;
std::string_view sv = s1;
std::string_view sub = sv.substr(3, 2);
println("s1={} s2={} sv={} sub={}", s1, s2, sv, sub);
// s1=abcdef s2=ef sv=abcdef sub=de
const char* cs = s1.c_str();
println("s1={} cs={}", s1, cs);
// s1=abcdef cs=abcdefstd::vector Type in the STLThe popular std::vector<type> structure allows
representing arrays with variable size (through the
push_back method). Example:
int v1[10];
int v2[] = {1, 2, 3, 4};
std::vector<int> k1{};
std::vector<int> k2 = {1, 2, 3, 4};
k2.push_back(999);
//
print("v[0]={} v[3]={} size={}\n", v2[0], v2[3],
sizeof(v2) / sizeof(v2[0]));
// v[0]=1 v[3]=4 size=4
print("k[0]={} k[4]={} size={}\n", k2[0], k2[4], k2.size());
// k[0]=1 k[4]=999 size=5
print("{}\n", std::is_aggregate<std::vector<int>>::value);
// falsestd::array Type in the STLJust like native arrays, e.g. int[], the aggregate
std::array<type, size> allows representing arrays of
fixed size. Example:
int v1[10];
int v2[] = {1, 2, 3, 4};
std::array<int, 10> a1{};
std::array<int, 4> a2 = {1, 2, 3, 4};
print("v[0]={} v[3]={} size={}\n", v2[0], v2[3],
sizeof(v2) / sizeof(v2[0]));
// v[0]=1 v[3]=4 size=4
print("a[0]={} a[3]={} size={}\n", a2[0], a2[3], a2.size());
// a[0]=1 a[3]=4 size=4
print("{} {} {}\n", std::is_aggregate<int*>::value,
std::is_aggregate<int[]>::value,
std::is_aggregate<std::array<int, 4>>::value);
// false true trueSo far, we have checked the following structures:
It is possible to return multiple elements (pair or tuple), through a structured binding with tuples:
auto double_it(int p) {
return std::tuple{p+3, p+6.5};
}
auto [x1,x2] = double_it(10); // x1=13 x2=16.5Q.: what is the return type of ‘double_it’?
A: std::tuple<int, double>.
Parameters are always copied (in C) when passed to a function or procedure. How do we pass complex types (structs and arrays with many elements) without losing time?
In these cases, the C language offers a special type called pointer.
The pointer syntax simply includes an asterisk (*) after
the type of the variable. An empty state is made with
nullptr (or 0).
Examples:
int* x = nullptr; struct P* p1 = nullptr;
A pointer simply stores the location (address) where a certain variable is stored in memory (basically, a number). So when a pointer is passed as a parameter, the copy of the pointer can be used to find the desired structure in memory.
The size of the pointer varies according to the architecture, but to address 64-bits, it takes up 8 bytes.
In pointers to aggregates, the access operator (.) is
replaced by an arrow (->). The &
operator takes the address of the variable:
Testing procedures f and g:
Programs frequently need to allocate more memory for use, which is stored safely in a pointer to the type of the memory:
The type of a function is basically a pointer (address) of the location of this function in the computer’s memory. For example:
This fact can be useful to receive functions as parameters, as well as to store anonymous functions (lambdas):
The C++ language allows member methods (member functions) with the inclusion of functions and variables inside aggregates (in C, functions must be external/global). To access fields of the aggregate from within these functions, use the pointer to the aggregate, called this:
Functions can call themselves again during their execution in a
recursive process. The implementation of member functions can
also occur outside the aggregate with the scope resolution notation
(::):
std::span Type in the STLLike std::string_view, for other arrays
int[], std::array and
std::vector, std::span supports
non-owning data sequences.
import std; // calling ./program 1 2 3
int main(int argc, char* argv[]) {
int v2[] = {1, 2, 3, 4};
std::span<int> s1{v2};
for (auto i : s1) std::println("{}", i);
// 1 2 3 4
std::vector<int> vec = {1, 2, 3, 4};
std::span<int> s2{vec};
for (auto i : s2) std::println("{}", i);
// 1 2 3 4
std::span<char*> input{argv, argc};
for (auto i : input) std::println("{}", i);
// ./program 1 2 3
return 0;
} // ============================================std::optional Type in the STLstd::optional<type> represents an optional value,
with allocation on the stack, not on the heap like
pointers (and smart pointers, which we will see next). Access
is done with the (*) operator.
auto find(char c, std::span<char> v) -> std::optional<int> {
// searches for char 'c' in an array v and returns the position
for (int i = 0; i < v.size(); i++)
if (v[i] == c) return i; // found
// not found
return std::nullopt;
}
// ...
std::vector<char> v = {'a', 'b', 'c'};
auto op = find('x', v);
if(op) println("position={}", *op);
else println("not found");std::expected Type in the STLstd::expected<type, error_type> represents an
expected value, with allocation on the stack, not on
the heap like pointers (and smart pointers, which we
will see next). Access is done with the (*) operator.
auto find2(char c, std::span<char> v) -> std::expected<int, std::string> {
// searches for char 'c' in an array v and returns the position
for (int i = 0; i < v.size(); i++)
if (v[i] == c) return i; // found
return std::unexpected{"not found"};
}
// ...
std::vector<char> v = {'a', 'b', 'c'};
auto exp = find2('x', v);
if(exp) println("position={}", *exp);
else println("{}", exp.error());Routines and aggregates can be exported to other compilation
units, through the word export. A module file has the
extension .cppm and begins with
export module MODULE_NAME;. Both conventional
.cpp files and .cppm modules can import
modules, with the keyword import.
C++20 brings the possibility of defining concepts (or concepts). This feature allows generic definitions about some type (including aggregate types with internal functions).
For example, we can create a concept HasNeg,
which requires the aggregate to have a neg() method:
Example of an aggregate conforming to the HasNeg
concept:
Thus, we can use a more specific concept instead of an automatic type:
auto a0 = Z{.x = 1}; // automatic type
auto p0 = new Z{.x = 1}; // pointer type
auto* p1 = new Z{.x = 1}; // pointer type
HasNeg auto a2 = Z{.x = 2}; // conceptual type
Z a3 = Z{.x = 3}; // explicit typeAnother form of type validation at compile time is
static_assert. For example, how do we guarantee that the
aggregate Z conforms to the concept HasNeg?
Important: the notion of concepts is fundamental for understanding abstract types, central in the data structures course.
std::move in C++Pointers are recognizedly problematic structures, therefore since the
C++11 revision it is recommended to use smart pointers instead
of native pointers. There are two kinds of smart pointers:
unique_ptr and shared_ptr. Both avoid the user
having to deallocate memory (with the exception of cyclic
structures, to be addressed in the future). To use them, you just
need to include the <memory> header, and replace
new with std::make_unique or
std::make_shared.
Pointers can be used as markers of an invalid memory space, generally
called null. In C, the NULL macro is generally
defined as zero, so it is a better practice to use the number zero
directly instead of NULL. The conditional can be used to
check a pointer as a boolean, which is the safest option. In C++, there
is std::nullptr, which can be used in specific situations
(generally smart pointers), but in general avoid
NULL and std::nullptr.
In C, it is only possible to pass variables by copy, which demands the use of pointers to avoid bulky and unnecessary copies.
In C++, there are the concepts of left-side reference
(&) and right-side reference
(&&). In short, we use a type& to
denote a reference to live data, and
type&& for a reference to data about to
die (or data in movement). This concept is fundamental to
deal with unique_ptr, since they do not allow copies, being
necessarily passed by reference.
To transform a live variable into a variable in
movement, you just need to use the std::move
command.
In addition to the classic routines, which return (or do not return) values, there are also coroutines, with the capacity to pause and resume execution.
One example is the fibonacci sequence, which starts from 0,
1, and continues with the sum of the last two elements. This is
an infinite sequence, and we can easily represent it with
co_yield returns of a coroutine with
std::generator:
To consume the values, just use the range for (all Fib less than 10):
Challenge: how do we implement this same functionality without a coroutine?
Challenge 2: another use is std::future
with coroutines connected to concurrent programming with threads. This
is a bit outside the scope of this course, but check out other
applications of coroutines and co_await.
Left-side references (lvalue) complement right-side references (rvalue). Observe:
Observation: there is also the
const type& syntax which allows lifetime
extension, something we will not explore in this brief review.
std::unique_ptr Typestd::unique_ptr<type> represents a unique pointer
to type (as if it were type*). A useful
function is get, which returns a native C pointer to the
data. The reset function deletes the pointer manually.
auto p1 = new int{10};
auto p2 = p1;
println("*p1={} *p2={}", *p1, *p2);
// *p1=10 *p2=10
delete p1;
auto u1 = std::make_unique<int>(10);
auto u2 = std::move(u1);
auto p3 = u2.get();
println("*u2={} *p3={}", *u2, *p3);
// *u2=10 *p3=10
u2.reset(); // deletes pointer u2 manually
u2 = nullptr; // deletes pointer u2 manuallyThe language’s standard library has components that are already
tested and in common use, solving several basic programming problems.
C++ has quite important implementations in its standard library, called
the STL. In the past, it was necessary to use
#include<...> to include these components, but since
C++23 it is possible to do it all automatically with an
import std, using the modern structure of CXX Modules.
We have already seen indirectly the use of some of these structures
in the course, such as: tuples in std::tuple; smart
pointers in std::make_unique or
std::make_shared; among other things. We have also seen
examples of very fundamental structures such as:
std::string and std::vector. Generally,
proposals are made by the community, and good implementations are
incorporated into the standard library, in future revisions of the
language.
std::shared_ptr Type (advanced)std::shared_ptr<type> represents a shared pointer
to type (as if it were type*). A useful
function is get, which returns a native C pointer to the
data. The reset function deletes the pointer manually. The
shared one allows copies and sharing, through reference
counting. Be careful with cycles, since they can lead to memory
leaks! For that, use std::weak_ptr or
cycles::relation_ptr (next). To use it, you just need
#include <memory>. Example:
std::function Type (advanced)The std::function<type> structure allows storing
functions, be it a lambda without capture (captureless lambda)
or a capturing lambda, also called a closure. A captureless
lambda can decay to a function pointer, while the others can only
be encapsulated as std::function. You just need
#include <functional>. Example:
// captureless lambda
int(*fsq1)(int) = [](int p) -> int { return p*p; };
std::function<int(int)> fsq2 = [](int p) { return p*p; };
// capturing variable x (by copy)
int x = 10;
int y = 20;
// closure x1 (returns x + 1)
std::function<int()> x1 = [x]() { return x+1; };
// capturing all local variables with =, y by reference
std::function<int()> fxy = [=, &y]() { y++; return x+y; };
int z = fxy(); // z==31 y==21this with C++23 (advanced)An interesting capability of C++23 is deducing this, which
allows working with typing over the this variable in
functions. This can be useful to capture this as a
reference, instead of a pointer, and also to name anonymous
functions.
std::scan (advanced/experimental)Just like std::print (currently from fmt),
there are proposals for a std::scan, currently in the scnlib
project by eliaskosunen.
The experimental proposal for C++26 is called P1729 “Text Parsing”,
and seeks to create a scn::scan function that replaces
scanf (by the same reasoning employed in the abolition of
printf). Example:
cycles::relation_ptr Pointer
(advanced/experimental)A smart pointer proposal to solve cyclic cases was created by prof.
Igor Machado Coelho, called cycles::relation_ptr.
This is an interesting project to understand the limitations of current
smart pointers, and what could possibly be improved in a future C++.
Example:
To use it, you just need
#include <cycles/relation_ptr>. Example:
We quote the C++ direction group, “DIRECTION FOR ISO C++” (2022-10-15), by H. Hinnant, R. Orr, B. Stroustrup, D. Vandevoorde, M. Wong (page 10):
C++ is seriously underrepresented in academia and often very poorly taught. It has been conventional to start teaching C++ by first introducing the lowest level and most error-prone facilities. Naturally, that discourages students and increases the time needed to get to what students consider meaningful computing (graphics, networking, mathematics, data analysis, etc.). Often, teachers even go to the extreme of insisting on using a C compiler. If the ultimate aim is to teach C++, that’s like insisting people start learning English by reading Beowulf or the Canterbury Tales in their original early-English language versions. Those are great books, but Early English is incomprehensible to most native Modern-English speakers.
We quote the C++ direction group, “DIRECTION FOR ISO C++” (2022-10-15), by H. Hinnant, R. Orr, B. Stroustrup, D. Vandevoorde, M. Wong (page 10):
In addition to the linguistic difficulties, such ancient sources present cultural conventions and idioms that seem very peculiar today. Instead of C, someone could teach Simula to prepare for learning C++. Why don’t people do that? Because the historical approach to teaching language (natural or programming language) complicates and detracts from the end goal: good code.
Why then do teachers use the C-first approach to teach C++? Part is tradition, curriculum inertia, and ignorance, but part of the reason is that C++ doesn’t offer a smooth path to idiomatic, proper, modern use of C++. It is hard to bypass both the traps of low-level constructs and the complexities of advanced features and teach programming and proper C++ usage from the start.
In summary: modern C++ is already absolutely superior to C in safety
and clarity, with equivalent performance, but historically it lacks good
structures for doing the basics (such as printing to
the screen, making arrays, etc.), forcing the use of unsafe structures,
such as pointers. So, recent revisions have pursued this goal, of making
basic usage easier (such as std::print,
std::array, std::string,
std::vector, smart pointers, …) and avoiding the need for
the C language to write basic programs.
Today (2023 with the C++20 standard) there are still problems, such as:
fmt::print and scn::scan)#include in basic code: the idea is
that, starting from the implementation of import std in
C++23, it will be unnecessary to include external libraries in basic
code :)Many will be solved in the next edition of C++ (but
scn::scan will still be missing), always keeping an eye on
good modern competitors such as Rust.
Any complex program needs division into parts, or modules, for greater control and verification of the correctness of the operations.
In this course, we will use a minimal modularization standard, so that it is possible to carry out tests on the code (in a systematic way).
A program starts at its “entry point” (or entrypoint),
typically an int main() function:
The declaration of functions can be done before the definition:
int square(int p); // declares the 'square' function
int square(int p) {
return p*p; // implements the 'square' function
}Declarations come in .h files, while the respective
implementations come in a .cpp file (or together as
.hpp).
main.cppWhen using GCC and an entrypoint in the
main.cpp file:
To compile:
g++ -std=c++23 -O3 main.cpp -o appMain
To run the code: ./appMain
Important: we consider a GNU/Linux system, but in
case it is Windows one can use the C/C++ compiler MinGW and run the
generated application with a .exe extension (Windows
executable standard).
Minimal modularization: 4 files.
main.cpp (tip:
put it in the src/ folder)src/ folder)test.cpp (tip: put them in the tests/
folder)makefile (with all: and test:
rules)It is also informative to have an extra file at the root with
explanations about the code (typically README.md in the
markdown language)
Important: the entrypoint file must contain
exclusively the int main() function (and its respective
#includes), to make code testing feasible.
During the course we will study several data structures, but whenever possible use the existing ones in the standard library (STL). They are “more efficient” and “error-proof”.
For example, it is easy to define an aggregate type
Pair, which holds two internal elements (generic type).
However, it is more advantageous to use the existing one in the STL,
called std::pair (the std:: prefix is called a
namespace and avoids name collisions):
assertDuring development, it is useful to check parts of the code with
simple tests that are necessary for its correctness (in real time). For
that, we can use assert(). Example:
In the same way, we can check types, especially concepts, at compile time:
A practical way to test modularized code with main.cpp
separated from rest.hpp is by using the Catch2 library.
You just need to create a test file, for example,
test.cpp:
To download the catch2.hpp file, just visit the project
site: github.com/catchorg/Catch2.
Direct link (August 2020):
github.com/catchorg/Catch2/releases/download/v2.13.1/catch.hpp
To compile:
g++ -fconcepts test.cpp -o appTests
To run the tests:
./appTests -d yes
0.000 s: Tests initialization of aggregate Z
===============================================
All tests passed (1 assertion in 1 test case)
Important: The options
-fsanitize=address and -g3 are recommended to
avoid bugs during development using GCC.
In this review about types, we sought not to go deep into any “advanced” characteristic of C/C++, although some concepts may seem new. Recommended topics (not covered in the course):
std::unique_ptr and std::shared_ptr (does not
require delete)Besides the course bibliography, we recommend (for this topic):
In particular, I thank the colleagues who elaborated good materials, such as prof. Fabiano Oliveira (IME-UERJ), and prof. Jayme Szwarcfiter whose concepts form the core of these slides.
I extend my thanks to the other colleagues who collaborated on the elaboration of the material of the Operations Research course, which paved the way for practical verification of this slide technology.
This course material is only possible thanks to the countless open-source projects that are necessary to it, including:
Special thanks to companies that support free projects involved in this course:
These slides were written using pandoc, according to the ilectures tutorial:
Except where expressly mentioned (with the due caveats for the material provided by colleagues), the license will be Creative Commons.
License: CC-BY 4.0 2020
Igor Machado Coelho