Variables and Mutability
We first saw variables in our mini guessing game project where we used them to store the guess of the user and create our PRNG etc.. Let's explore what happens when we try to modify constant data and when we would want to allow mutations.
Create a new project have done before, with a main.cxx and CMakeLists.txt and add the
following contents. This will act as out scratchbook project for tinkering with examples.
I won't always go into super detail about what changes will be made between various
topics but most examples will have a full example with some being hidden behind snips
which can be exposed using the 'eye' button in a codeblock.
cmake_minimum_required(VERSION 3.22)
project(main
VERSION 0.1.0
DESCRIPTION "C++ Book Examples"
LANGUAGES CXX)
add_executable(main main.cxx)
target_compile_features(main PRIVATE cxx_std_17)
if (MSVC)
# warning level 4
add_compile_options(/W4)
else()
# additional warnings
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
#include <iostream>
auto main() -> int {
return 0;
}
Declaring Variables and Constants
To start with we will explore how your declare variables and constants and what happens
when you try to change the value for each. Below outlines a program that declares a
constant with the name x and assigns it the value 42 and displays the result. Very
straight forward, basically the same as 'Hello, world!'. We then reassign x to 43 and
print the result. The question is, would this compile? Let's take a look.
#include <iostream>
auto main() -> int {
const auto x = 42;
std::cout << x << "\n";
x = 43;
std::cout << x << "\n";
return 0;
}
When we try to compile the above we should get an error like so:
$ cmake -S . -B build
...
$ cmake --build build
[ 50%] Building CXX object CMakeFiles/main.dir/main.cxx.o
/home/user/projects/common/main.cxx: In function ‘int main()’:
/home/user/projects/common/main.cxx:7:7: error: assignment of read-only variable ‘x’
7 | x = 43;
| ~~^~~~
gmake[2]: *** [CMakeFiles/main.dir/build.make:76: CMakeFiles/main.dir/main.cxx.o] Error 1
gmake[1]: *** [CMakeFiles/Makefile2:83: CMakeFiles/main.dir/all] Error 2
gmake: *** [Makefile:91: all] Error 2
The answer is a definitive no. Why? Because x in this program is a constant meaning its
value does not change over its lifetime. If x is meant to change we drop the const
keyword, allowing x to be mutated and thus making it a variable.
#include <iostream>
auto main() -> int {
auto x = 42;
std::cout << x << std::endl;
x = 43;
std::cout << x << std::endl;
return 0;
}
$ cmake -S . -B build
...
$ cmake --build build
...
$ ./build/main
42
43
Type Deduction
It is time to address the elephant in the room, auto. You may be asking;
- "What is this peculiar keyword and why are we declaring constants and variables with it?"
- "I thought C++ was a typed language, where are the types?" etc..
All valid questions and if we are to continue using auto we should address what it is
doing. auto does not introduce dynamically typed variables into C++, the type is
still there it is just inferred by the compiler. This is a mechanism called type
deduction and it allows for the type of many expressions to be resolved by the compiler
rather than the programmer. This feature was introduced with C++11 allows for simpler
expressions to be written in the language.
Take a look at the example below, both lines declare a variable with one explicitly
stating the type of x while the other lets the compiler deduce the type of y for you
but with both variables having the type int.
int x = 5;
auto y = 6;
As discussed in the chapter 02, you can still mark the type of an expression explicit by
making it explicitly known on the right hand side of the equals sign, like we did for
std::string.
int x = 5;
auto y = int{6};
Storage Duration
The primary goal of variables (and constants) is to store data. Whether that be numbers, characters, memory address etc.. How long a variable is around for (known as its lifetime) is determined by where space for the data was made available from and can be labelled by one of three categories known as "storage durations".
So far we have seen data with automatic storage duration, which means the lifetime of
that data is tied directly to the scope it was declared in. Currently we have only been
working in the scope of the main() function, thus when we return from main(), the
variables we have declared will become unavailable.
Soon we will look at data that has dynamic storage duration. This is data whose lifetime is managed manually by the program and thus by the programmer. You must explicitly request the space for the data and remember to return it once you no longer require it.
These two storage duration categories are often tied to the notion of the stack and the free store respectively. Data with automatic storage duration is allocated on the stack allowing for the usual mechanisms of the stack to handle the allocation and deallocation of the slots used for the data, and the free store being the region in which dynamic data is located.
One we haven't looked at yet is static storage duration. This is data that is encoded
directly in the binary of a program and thus lives for the entire duration of the
program. To give data this storage duration we declare it with the static keyword.
Global variables declared outside of a functions are implicitly static.