Lab 1: Setup & C++

Github Classroom assignment

Please put your answers to written questions in this lab, if any, in a Markdown file named README.md in your lab repo.

Introduction

Welcome to CS 1230!

The purpose of this lab is twofold: to set you up with what you'll need to work on assignments locally, and to ease you into C++, the programming language we'll be using in this course.

If you have any questions or run into any issues, please let us know over Ed or during TA hours. We'll do our best to help!

Objectives

  1. Complete our Getting Started form,
  2. Install Qt & Qt Creator locally on your computer,
  3. Build and run a starter Qt program using the Qt Creator IDE, and
  4. Learn about the basics (and subtleties) of C++.

Getting Started

We assume that you're already familiar with using Github Classroom to accept assignments, Ed to ask questions, and Gradescope to submit work. If you need a refresher on any of this, check out this page.

Please fill out our Getting Started form as you complete the steps below:

  1. Read our course's collaboration policy,
  2. Join the CS 1230 Ed discussion page
  3. Enroll in this course over on Gradescope using our entry code, E788WB,
  4. Accept this lab's assignment from Github Classroom, and
  5. Clone the resulting repository to your local machine.

At this point, you should have a copy of this lab's repository. All set? Let's get started!

Qt and Qt Creator

In CS 1230, we will be using Qt and Qt Creator to develop, build, and run all our projects and labs. Before we walk you through how to install them to your local machine, here's a brief description of each:

  • Qt is a software used for building graphical user interfaces (GUIs) and cross-platform applications, e.g. for smart TVs or in-vehicle displays.
  • Qt Creator is an integrated development environment (IDE) included with each Qt install. It provides useful tools for developing in C++, which you'll learn about later in the course.

We will be using Qt 6.5, which is the latest long-term support version. As for Qt Creator, any version is fine—it'd be easiest to just use the one that comes with your Qt install.

But I'd rather work in VS Code / Emacs / Notepad++ / etc, instead of in Qt Creator!

If you know what you're doing, you may certainly write code in your IDE of choice. In the past, students have successfully used other IDEs for writing code, before using Qt Creator to build and run their projects.

That being said, the CS 1230 course staff are not obligated to provide support for other IDEs and, if needed, will build and run your code with Qt Creator. Thus, you'll probably want to install Qt Creator anyway, if only to test that your code works as expected when running with it.

Installation

If you're using a department machine, skip this section. Simply open a terminal and run the following to open Qt Creator:

qtcreator

Qt and Qt Creator will take up ~3GB of space in total.

Warning for students using macOS

Before start installing Qt, please first use the following command to check whether you have Xcode or the Xcode Command Line Tools installed.

xcode-select -p

If your console outputs either of the following, which means that you have already installed some version of Xcode, you are good to continue.

/Applications/Xcode.app/Contents/Developer
/Library/Developer/CommandLineTools

Otherwise, please follow our Xcode CLT Installation Guide before proceeding.

Instruction
Screenshot (click to expand)
Download and open the appropriate Qt installer for your operating system from the Qt installer page.todo
Follow the instructions on the installer to create a free Qt account (or use an existing one).todo
When prompted, opt for a Custom Installation.todo
On the next page, select (at least) these four:

1. Qt 6.5.2 > MinGW [...] (Windows) or Qt 6.5.2 > macOS (macOS),
2. Dev. and Designer Tools > Qt Creator
3. Dev. and Designer Tools > CMake
4. Dev. and Designer Tools > Ninja

Note: you may opt to install more components, but be warned that this will take up a lot more space on your machine!
todo
Proceed with the installation. Once finished, launch Qt Creator.todo

Setting your build directory

By default, when you build and run your projects, Qt Creator will create the build output in the parent directory of your project directory. For example, if we had this Lab 1 project at /Users/jo/cs1230/lab01-setup-cpp-jo, Qt Creator would place the build directory at /Users/jo/cs1230/build-lab01-setup-cpp-jcarberry... by default. For some of the projects we have in this course, this structure can be somewhat inconvenient or hard to work with, so instead we opt for a format where all builds live inside a build folder inside the project directory. So, for the lab01 project, we'd have the following structure:

∟ lab01-setup-cpp-[your GitHub username]/ (project directory)
  ∟ build/ (all build output goes here)
    ∟ build-[project name]-[Qt kit name]-[build configuration]/ (a build directory)
      ∟ lab01 (the executable)
      ∟ [other build files]
  ∟ src/
  ∟ CMakeLists.txt
  ∟ [other files]

In order for Qt Creator to use this structure by default, we need to change a setting in the preferences.

Instruction
Screenshot (click to expand)
Open Qt Creator preferences by going to Qt Creator > Preferences... or ⌘ + , (Mac) or Edit > Preferences... (Windows).todo
Open the Build & Run > Default Build Properties.todo
In the Default build directory field, replace the current text with: ./%{JS: Util.asciify("build/build-%{Project:Name}-%{Kit:FileSystemName}-%{BuildConfig:Name}")}.todo
Click OK.todo

If you have already configured a project in Qt Creator before making this change and the project is not building to within [project_directory]/build, you can reconfigure your project by:

  1. Closing the project (File > Close Project ...)
  2. Deleting the CMakeLists.txt.user file in your project directory
  3. Reopening the project (File > Open File or Project...)

You will be prompted to configure the project again, and it should now build to the correct directory.

Opening a Project

Instruction
Screenshot (click to expand)
In the Qt Creator application, click Open Project. In the file browser that appears, choose the CMakeLists.txt file located in your copy of this lab's repository.todo
You will then be asked to select a kit. If there are multiple options, select the one that has 6.5.2 in the title. Expand the details dropdown on the right and make sure that, at least, the Debug and Release options are selected.todo

After that, you should be taken to the Edit tab!

Using Qt Creator

In the left sidebar, Qt Creator displays content based on the currently-selected 'sidebar view'. The two views you'll find mose useful are the Projects and File System views. Projects view shows a list of projects open in the current session, and the source, header, and other files for each; meanwhile, File System view shows all files in the currently selected directory.

We recommend using the Projects view most of the time! You can switch views by clicking on the dropdown menu at the top.

TODOTODO
Figure 1: Projects view vs File System view

Project Settings

To modify the currently-selected project's settings, click 'Projects' on the left-most sidebar of your screen. These settings are divided into 'build' and 'run' settings.

In run settings, you can edit your command line arguments, which will be important for later assignments. For now, the most important setting you can edit here is your working directory.

For all assignments in CS 1230, you should always change your working directory to the parent folder of your project's CMakeLists.txt.

TODO
Figure 2: Always remember to set your working directory!
Extra: what is the significance of the working directory?

C++ is a compiled language, so an executable is produced at the end of the build process. The working directory is the directory from which this executable will be run.

Two important points follow:

  1. This executable will be generated in a new build directory, which will be a "sibling" of your project's source directory. For example:
∟ build-lab01_setup_cpp_ghusername-<kit-name>-Release/ (build directory)
  ∟ lab01                                              (the executable)
∟ lab01-setup-cpp-ghusername/                          (source directory)
  ∟ CMakeLists.txt                                     (the file you opened in Qt Creator)
  ∟ src/
  ∟ resources/
    ∟ images/
  1. By default, your project's working directory will be set to this build directory.

This is a problem because your application might try to access files using a file path relative to the source directory, but incorrectly looks for those files relative to the build directory.

Meanwhile, in build settings, you can select your build configuration from Debug, Release, and (possibly) others. A Debug build contains additional information internally that will be important if you're using the debugger, whereas a Release build is optimized to run faster.

We recommend using the Release configuration for all non-debugging work. Note that you can set the build configuration using a dropdown on the bottom left:

TODO
Figure 3: Select your build configuration here.

Running Your Project

Finally, you can run your project by hitting "Run"; this is the green play button in the above image. This will run your project in whatever build mode is currently selected.

When you're ready, hit "Run".

You should see that the application runs and exits with error code 0 (no error).

You just ran a simple C++ program! Of course, it did nothing, but we can easily change that in the next section 🙂.

C++

Hello World

A simple C++ program that does nothing (like the one you just ran) looks something like this:

// A C++ program always starts from the main() function.
// main() returns an integer indicating how the program exited
int main() {
  // Though this function body is empty, it still returns an int!
  // In C++, if control reaches the end of main() without encountering
  // a return statement, the effect is that of executing `return 0;`
}

In order to modify this function to print "Hello world", we must first include the input/output library (iostream), which is a part of the C++ standard library.

#Include-ing Files

In C++ files, you can #include other files to gain access to functions, types, macros, and variables declared in those other files. For example, in our Hello World program, we can #include the iostream header file at the top of our main.cpp file like so:

#include <iostream> // Lines that begin with `#` do not require semi-colons

Note the use of angle brackets around iostream. In C++, you should use angle brackets for standard library files, but you should use double quotes for files within your own project.

Extra: how the compiler interprets angle brackets vs. double quotes

When you use angle brackets, most compilers will search for the file in the include path list. When you use double quotes, they’ll first search the local directory (i.e. the directory where the module being compiled is) and only then search the include path list.

Accessing Things in a Namespace

Imported functionality is usually grouped under a namespace, and we can access things within a namespace using the double-colon (::) operator. For example, since we included iostream in our Hello World program, we now have access to the following things in the std namespace:

  • std::cout: prints things to stdout, i.e. the terminal or the "output" window in Qt Creator.
  • std::endl: inserts a newline character and flushes the output stream.

std::cout is usually used in tandem with the insertion operator (<<). This inserts characters into the current output stream, and it works like string concatenation with + in some languages.

The insertion operator can also be used with std::endl to insert a newline, in place of \n, the difference being that \n does not flush the output stream.

We are now ready to write something to the terminal.

  1. Make sure iostream is included in main.cpp.
  2. In main(), use std::cout and std::endl to print "Hello, world!" to the terminal.
  3. Run your program, and verify that it behaves as expected. Hello, world!
Extra: two other potentially useful iostream objects
  • std::cin reads from stdin (terminal input), in a blocking manner, and
  • std::cerr prints to stderr (terminal error messages)

std::cin is usually used in tandem with the extraction operator (>>). It's highly unlikely that you'll use this in CS 1230, though.

Now, we can get into the fun stuff!

Primitive Types

C++ is a typed language. It comes with several primitive types, including...

  • integer-like types (integers, booleans, characters)
  • floating point types
  • arrays (specifically, C-style arrays)
  • functions and lambdas
  • pointers and references

Some of these you might already know from languages you've learned before. Others, such as pointers and references, are C++ concepts that we'll expand on in later sections.

Note that strings are not a primitive type in C++, as string literals are simply char arrays. However, the standard library does provide the std::string type, which allows us to perform common string operations.

Variables

When defining a variable, we have to declare its type.

int x = 42;
double y = 3.14;

Alternatively, we may use auto to tell the compiler to deduce the variable's type, based on how we've initialized it. This could be useful if its type has a very long name, or if we're not sure about its exact type.

auto z = 2.71; // type of z deduced as double

// I'm not exactly sure about the type of this string literal
auto w = "random string abcd";

Functions

The same rules also apply to functions: we must declare their return types and the types of each parameter.

int plusOne(int x) {
  return x + 1;
}

Just as with variables, we may use auto in place of a specific return type—the compiler will deduce the return type from the return statement in the function body.

auto plusOne(double x) { // Using auto instead of a specific return type
  return x + 1;          // Return type deduced as double
}
Overloading

Defining multiple functions with the same names, but different type annotations, allows you to do something called overloading, which you might be familiar with from languages like Java.

int plusOne(int x) {
  return x + 1;
}

auto plusOne(double x) {
  return x + 1;
}

auto x = plusOne(42); // calls plusOne<int>
auto z = plusOne(2.71);  // calls plusOne<double>
Generic Functions

We can improve the code in the example above by making our function generic. This is easy to do in C++: we simply change the type of its input parameter to auto.

auto plusOne(auto x) {
  return x + 1;
}

auto a = plusOne(1230); // a == 1231, instantiates plusOne<int>
auto b = plusOne(6.5f); // b == 7.5f, instantiates plusOne<float>
auto c = plusOne(3.14); // c == 4.14, instantiates plusOne<double>
Extra: what actually is an auto parameter?

A function with at least one auto parameter, such as our generic plusOne, is known as an abbreviated function template.

Extra: Lambdas

There are also function-like entities in C++ called lambdas. We'll not explain them in detail since functional programming is outside the scope of this lab. However, you are welcome to play with them and ask questions about them on Ed.

You may need to use lambdas for certain extra credit features, such as multithreading, in your future assignments. To get you started, a toy example is shown below.

Example
auto plus(auto increment) {
  return [=](auto x) {
    return x + increment;
  };
}

// Observe that functions are "first-class" in C++
auto apply(auto operation, auto x) {
  return operation(x);
}

auto x = apply(plus(20), 22); // x == 42
auto y = apply(plus(-1.1), 4.24); // y == 3.14

You can find more information about lambdas here.

A Warning About Type Deduction

Type deduction is very powerful in C++. For instance, most typed languages do not allow type deduction on function signatures like C++. However, overusing it has a negative impact on the readability and maintainability of your code, and it can cause unexpected compilation errors/crashes.

Remember that C++ is a statically typed language, meaning the type of a variable (e.g. int, float, etc.) cannot change after declaration; this applies to variables whose types are deduced using auto!

You should be very careful to strike a balance between type declaration and type deduction to maximize your code clarity. We recommend only using auto for "generic" functions and in cases when you are certain of the behavior of the deduced type.

Whenever possible, explicitly declare types in function signatures. Not doing so can lead to subtle, silent bugs which can be difficult to spot.

We can now write functions and use them to process different things!

  1. Write a function timesTwo which takes an int, returns an int, and does what the function name suggests.
  2. Add std::cout << timesTwo(21) << std::endl; to your main function.

You should see 42 when you run the program.

Let's use what we learned and make timesTwo more interesting!

  1. Change the signature of timesTwo and make it generic.
  2. You might also need to change the definition in the function body of timesTwo if you used multiplication for the previous task. Note that multiplication is not defined for std::string, but addition is, so how do you express "times 2" in the form of addition?
  3. Add the following print statements to your main function:
std::cout << timesTwo(123) << std::endl;
std::cout << timesTwo(3.14) << std::endl;
std::cout << timesTwo(std::string{ "abc" }) << std::endl;

You should see 246, 6.28, and abcabc when you run the program.

Structs and Classes

Going beyond primitives, we can create custom types in C++ by combining existing types and bundling them with functions.

These custom types are known as structs/classes, and they can be defined using the struct/class keywords respectively.

Structs and classes are almost the same things in C++, with the only difference being that structs have public member access by default and classes have private member access by default. The basic form of a struct is shown as follows:

struct Rectangle {
  double length;    // A data member, also known as a field
  double width = 1; // Fields can have default values
  // Note: fields must be explicitly typed; you cannot use type deduction here

  // A member function, also known as a method
  double calculateArea() {
    return length * width;
  }

  // This member function modifies the struct instance's state
  void makeItASquare(double sideLength) {
    length = sideLength;
    width = sideLength;
  }
};

Here's how we can create instances of Rectangle:

// Create an instance of Rectangle
auto x = Rectangle{ .length = 2, .width = 4 };

// Field names can be omitted. Values in the brackets will
// be assigned to each field sequentially

// Equivalent to Rectangle{ .length = 4, .width = 3 }
auto y = Rectangle{ 4, 3 };

// Equivalent to Rectangle{ .length = 5, .width = 1 },
// because of the default value
auto z = Rectangle{ 5 };

And here's how we can use those instances:

// Remember that structs have public member access by default.
// If this Rectangle was a class, you'd have to declare the
// relevant fields/methods public to do this

// Getting and setting fields
auto oldLength = x.length;        // oldLength == 2
x.length = 4;

// Calling member functions
auto newArea = x.calculateArea(); // newArea == 16
x.makeItASquare(oldLength);       // x.length == x.width == 2
A warning about constructors and other special member functions

If you have any experience in class-based OOP, you probably know that you can use constructors to initialize an object, instead of initializing it field-by-field (aggregate initialization).

However, we generally recommend that you do not manually define your own constructors.

Instead, we suggest using aggregate types where possible.

Improper handling of constructors and other special member functions* can break value semantics for your type, and cause unexpected bugs or resource leaks.

And, while it is entirely possible that you may never encounter or notice such a bug even if you do use special member functions, we still recommend using aggregate types as the less error-prone option.

* special member functions = default constructors, copy constructors, move constructors, copy assignment operators, move assignment operators, and destructors

Let's add more behaviors to our Rectangle type and enhance its capabilities!

  1. Add a method calculatePerimeter to Rectangle.
  2. Add std::cout << Rectangle{ 7, 8 }.calculatePerimeter() << std::endl; to your main function.

You should see 30 when you run the program.

Now that we've seen what we can do with Rectangle, are you ready to create a new type from ground zero?

  1. Create a Circle type using the struct keyword.
  2. Circle should contain a field radius of type float, and two methods calculateArea and calculatePerimeter.
  3. Tip: to use , you can use std::numbers::pi.
  4. After completing your Circle type, create a few instances of Circle in your main function, and call some of their methods.

See if your Circle instances exhibit the expected behaviors when you run the program.

Now, we have 2 types Rectangle and Circle, with the same member functions calculateArea and calculatePerimeter. Can we define one printShape function that works for both types?

Generic Functions (Reprise)

If you have previous experience in OOP, you might think of defining a Shape interface with a printShape() abstract function, and have both Rectangle and Circle both implement Shape. This is unnecessary in C++.

Remember the generic functions we learned in the previous section? If a function has a parameter of auto type, it is allowed to be anything! We can pass Rectangle and Circle instances to it just like that!

Extra: more on templates, for those familiar with programming languages

The reason behind this magic is that C++ templates are structurally typed, and they do not enforce parametricity. The lack of parametricity means when a type parameter gets reified by an actual type T, the type information of T is not erased in the body of the function template. This allows template definition to rely on any ad-hoc property of T, including inspecting what T actually is. Such capability enables templates to simulate features commonly seen in a dependently-typed system, or an untyped language.

// a function template that only accepts containers with no more than 42 elements
// simulating a dependently typed function
auto f(auto x) requires (x.size() <= 42) {
    // empty
}

f(std::array<int, 10>{}); // OK
// f(std::array<int, 43>{}); <- error

// a function template whose return type depends on its input
// simulating an untyped function
auto g(auto x) {
    // if the input can be invoked like a function
    // the return type is the same as the input's return type
    if constexpr (requires { x(); })
        return x();
    // otherwise return type is int
    else
        return 42;
}

auto x = g([] { return 2.71; }); // x is of type double
auto y = g("aaa"); // y is of type int

Navigate to the empty function printShape.

void printShape(auto shape) {
  // Your code here
}

Complete its definition, so that when you pass in either a Rectangle object or a Circle object, it prints:

Area: /* area of the shape */
Perimeter: /* perimeter of the shape */

Call printShape in main() with different Rectangle and Circle instances, to see if printShape exhibits the expected polymorphic behavior.

Other Standard Library Utilities

Besides iostream, the C++ standard library provides us with many useful utilities, and we'll focus on the four most commonly used ones: its containers and strings.

Arrays

std::array is a fixed-length array.

Example
#include <array>

auto x = std::array<int, 3>{}; // Must declare element type and length

// Element type and length can be deduced if you immediately initialize
// the array with values
auto y = std::array{ 3.14f, 2.71f };

// Getting and setting array elements
auto z = y[0]; // z == 3.14f
x[0] = 42; // now, the zeroth element of x is 42

auto [a, b, c] = x; // Arrays can be unpacked: a == 42, b == 0, c == 0

// Commonly used array methods
auto lengthX = x.size();
auto underlyingPointer = x.data(); // we'll explain pointers later

// Arrays can be looped over element-wise
for (auto element : y) {
  std::cout << element << std::endl; // prints 3.14, then 2.71
}

Vectors

std::vector is a dynamic-length array. Unlike std::array, it allows us to insert or remove elements at any time, and it has mostly the same capabilities as std::array. However, std::array is slightly more performant as it doesn't require dynamic memory allocation.

Example
#include <vector>

auto x = std::vector<int>{}; // Must declare element type

// Element type can be deduced if you immediately initialize
// the vector with values
auto y = std::vector{ 3.14f, 2.71f };

// Manipulating vector elements
auto z = y[0]; // z == 3.14f
x.push_back(42); // add an element to the end of the vector
x.push_back(123); // add another element after the 42 we just inserted
x.pop_back(); // remove the element we just added

// Commonly used vector methods
auto lengthX = x.size();
y.reserve(20); // Pre-allocate memory for more elements. Its length stays the same
x.resize(10); // Resize the vector, actually changing its number of elements (length)
auto underlyingPointer = x.data(); // we'll explain pointers later

// Vectors can be looped over element-wise
for (auto element : y) {
  std::cout << element << " "; // prints 3.14, then 2.71
}

Tuples

std::tuple is a heterogeneous container—it is capable of storing elements of different types. It is most commonly used to achieve multiple return values in C++.

Example
#include <tuple>

auto makeTuple(auto x, auto y) {
  return std::tuple{ x, y };
}

auto [x, y] = makeTuple(42, 3.14); // tuples can be unpacked, x == 42, y == 3.14

// This is a variadic function template: it takes any number of arguments, each of any type
// It is unlikely that you'll need to use variadic functions for this course
auto doubleEach(auto ...x) {
  return std::tuple{ x + x... };
}

auto [a, b, c] = doubleEach(12, 2.71, std::string{ "abc" });
// a == 24, b == 5.42, c == "abcabc"

Strings

std::string provides basic string operations in C++. You can find its documentation here.

Example

The example below shows you how to create string objects, or convert string literals to std::string

#include <string>

auto x = std::string{}; // Create an empty string
auto y = std::string{ "hello!" }; // Convert a string literal to a std::string

using namespace std::literals; // Use this namespace to create std::string literals
auto z = "abcd"s;              // Create a std::string literal, note the s suffix
z += "efgh";                   // Use + to concatenate strings

Iterating Over Containers

Defining containers is one thing, but we also need to know how to operate over the elements. There are two primary methods for iterating: the range-for loop, and index-based iteration.

// range-for
for (auto element : container) {
  // use element
}

// index-based
for (auto index = 0; index < container.size(); ++index) {
  auto element = container[index];
  // use element
}

Note that a more efficient way of doing the range-for loop is to use an ampersand after auto. You will learn more about what this means in the section on references.

// reference based range-for
for (auto& element : container) {
  // use element by reference
}

Now that we've learned the basics of containers and strings, let's try using them!

  1. Create an array of std::strings. You're free to pick either std::array or std::vector.
  2. Fill the container with some strings.
  3. Apply timesTwo to each string element in the container. Hint: you cannot use the range-for loop you saw above to modify container elements, you'll see why in the following section when we explain references.
  4. Print each string element in the container, and see if the result is what you expect.

Pointers and References

Every entity in our program, from variables to functions to constants etc, all exist somewhere in memory, and they all have a unique memory location called a memory address.

A pointer is an integer storing a memory address, and it allows us to manipulate the object at that address. We can obtain a pointer to almost anything in C++ by taking its address using the address of operator &. The obtained address will be of a pointer type, denoted by the target object type followed by an asterisk *.

int x = 42;
int* px = &x; // px is a pointer to an integer, pointing to x
auto px2 = &x; // type deduction works for pointers too, type of px2 deduced to int*
auto* px3 = &x; // partial type deduction works too, px3 is a pointer to some deduced type, auto deduced to int
// pointer variables themselves also reside somewhere in memory, you can get a pointer to pointer
auto ppx = &px; // ppx is of type int**, a pointer to a pointer to an integer
// let's see where x is located in (virtual) memory!
auto MemoryAddressOfX = reinterpret_cast<unsigned long long>(px); // cast pointer to largest integer type
std::cout << MemoryAddressOfX;

The first thing we can do with a pointer is to access the entity at the address that the pointer points to. This can be achieved by using the dereference operator which also has the form of a star *. For pointers to non-primitive types, we can also use -> to access its members.

auto x = 42;
auto y = Rectangle{ .length = 4, .width = 2 };
auto px = &x;
auto* py = &y; // you could still write out * along with auto to make it extra clear that it is a pointer.

// This is how we access the object that a pointer points to
auto x2 = *px; // x2 == x == 42
auto y2 = *py; // y2 == y == Rectangle{ .length = 4, .width = 2 }
auto a = py->calculateArea(); // same as y.calculateArea()
auto yLength = py->length; // same as y.length

Dereferencing a pointer also allows us to modify the object that it points to

auto x = 42;
auto y = Rectangle{ .length = 4, .width = 2 };
auto px = &x;
auto py = &y;

*px = 123; // this sets x to 123
py->width = 3; // this sets y.width to 3

std::cout << x; // you should see 123 here
std::cout << y.width; // you should see 3 here

Let's see what we can do with pointers!

Navigate to the empty function inplaceTimesTwo. It has the same functionality as our previous generic timesTwo, however, it modifies the input variable in place rather than returning a new value.

void inplaceTimesTwo(/* ??? pointerToSomeVariable */) {
    // your code here
}

auto x = 21;
auto y = std::string{ "abcd" };

inplaceTimesTwo(&x);
inplaceTimesTwo(&y);

std::cout << x; // you should see 42 here
std::cout << y; // you should see "abcdabcd" here
  • Uncomment the pointerToSomeVariable parameter, replace ??? with a proper type declaration. (hint: both auto and auto* can represent a pointer of any type).
  • Complete the function body using what we learned.
  • Uncomment the supporting code in main() for task 9, execute the program and see if you get the expected result.

You might've noticed that pointers are somewhat unwieldy to use; we have to first take the address of something, then dereference the pointer. This can be simplified by the use of references. A reference is essentially a dereferenced pointer, meaning the entity at a particular memory address. You can find more information about the connection between pointers and references here. The reference type is denoted by the entity type followed by &.

int x = 42;
int& refx = x; // a reference to x, it has the same memory address as x
auto& refx2 = x; // we may use type deduction with references, type of refx2 deduced to int&

refx = 123; // this sets x to 123 because refx and x share the same memory address
std::cout << x; // you should see 123 here

One thing we have to be careful about with references is that we always have to spell out & in the type declaration when creating a reference, whether we're using type deduction or not. Otherwise, we'd be creating a copy rather than a reference.

int x = 42;

int& refx = x; // a reference to x, same memory address as x
auto& refx2 = x; // a reference to x, same memory address as x

int y = x; // this is a copy of x! it's a new variable with its own unique address!
auto y2 = x; // again, it's a copy with its own unique address!

This is because C++ has value semantics by default.

Extra: To be precise...

This is known as lvalue-to-rvalue type decay in C++ terminology.

Now that we know C++ makes a copy when creating something from another unless specified otherwise (i.e. creating a reference). We should really change most of the parameter types in our function signature to references unless it's something trivial like an int or double. Otherwise, a full copy will be made for every object we passed to the function, and it could lead to serious performance problems!

void func(std::vector<int> things) {
    // empty
}

void betterFunc(std::vector<int>& things) {
    // empty
}

auto things = std::vector{ 1, 2, 3, 4 };

func(things); // 'things' will be shallow-copied when you call func(), because it gets passed by value!

betterFunc(things); // no copy will be made here! because 'things' gets passed by reference!
// This also means if betterFunc modifies 'things' in any way in its function body, it will be reflected here

The same also applies to looping over containers. You need to use references in a range-for loop to modify elements.

auto things = std::vector{ 1, 2, 3, 4 };
// this copies every element in 'things'
// not ideal if the element type is not trivial to copy
for (auto x : things) {
    std::cout << x << std::endl;
}

for (auto& x : things) { // this loops over each element by reference
    x += x;              // which also enables you to modify the elements
}

// now things = [2, 4, 6, 8]

Let's try our hand at references!

  1. Navigate to the empty function doubleEachElement, this function takes any container and doubles each element in the container.
void doubleEachElement(/* ??? container */) {
    // your code here
}
  1. Uncomment the container parameter, and replace ??? with a proper type declaration. (hint: references work with generic parameters too!).
  2. Complete the function body using what we learned.
  3. Pass different std::array and std::vector objects to doubleEachElement in your main function.

Print the results after doubleEachElement calls, and see if it matches your expectation!

Debugging

The programs you'll write in this class will often crash or exhibit weird behavior due to bugs. Debugging is a vital skill, and learning to efficiently debug will go a long way. While printing out variable values to the terminal is a valid debugging method, it can quickly introduce clutter and make it difficult to diagnose deeper issues.

We strongly recommend using QtCreator's built-in debugger. Like most other debuggers, it allows you to interactively pause program execution at breakpoints, step through your programs line by line, and inspect variable values at every point.

Let's use the QtCreator debugger to fix a problematic function.

  1. Change your build configuration from Release to Debug.
QtCreator Debug Mode
Figure 4: QtCreator Debug Build

We've provided you with a buggy function called generateSequence which is supposed to output the sequence (1, 1/2, 1/3, ..., 1/n) contained in a vector.

  1. In main(), uncomment the calls to generateSequence() and verifySequence().
void main() {
  // ...
  // code for previous tasks


  // Task 11:
  std::vector<float> sequence = generateSequence(5);
  verifySequence(sequence);
}
  1. Run the program. You should see an error message in your terminal :(

Let's see how we can use the debugger to figure out what's wrong with generateSequence.

  1. Set a breakpoint at the start of the generateSequence function by clicking the region to the left of your chosen line number.
  • A red circle should appear indicating that the program will pause execution at that line when run in debug mode.
  1. Run the program in debug mode by pressing the button above Build:
Button to start debugging session
Figure 5: Button to start debugging session

The program should run up to and pause at your breakpoint. You should see something similar to the following:

QtCreator Debug Mode
Figure 6: QtCreator Debugger Panels.

The debugger will display several panels. The ones you'll most often interact with (highlighted in Figure 6) are the:

  • Locals: this is a list of the variables accessible in your current scope. You can examine their types and current values. Note that some may not be initialized yet and will have arbitrary values!
  • Execution control buttons: these buttons allow you to resume/pause/stop the program and execute individual lines of code.
  • Call stack: this shows you where you currently are in the execution of the program. In this case, you are in generateSequence, which was called from main.

Execute each line of generateSequence one at a time by repeatedly pressing the "step over" button.

QtCreator Debug Mode
Figure 7: Step Over button in the execution control panel
  • Pay attention to how the variable values in the locals panel update after each line is executed - You can view the elements inside a container (e.g. std::vector) by clicking on the triangle next to their variable name in the locals panel once the variable has been initialized.
  1. Find and correct the bug! You only need to edit the code in generateSequence.

    • Remember that the desired output sequence is (1, 1/2, 1/3, ..., 1/n)
    • You can always restart the debugger by pressing the green power icon in the execution control panel.
  2. Verify that you've fixed the bug by letting the program finish executing. You should no longer see the error message.

Hint

Pay special attention to the temp variable. It will be helpful to use the debugger to inspect its properties.

It may have been possible to spot the bug simply by inspection in this task, but this is seldom the case in more complex codebases like the ones you'll build for your projects: bugs can be far subtler, so getting comfortable with the debugger will save you significant time and tears!

End

Congrats on finishing the Setup & C++ lab! Now, it's time to submit your code and get checked off by a TA. If you wish to have a deeper understanding of C++ or learn advanced C++ features to better your program design, you can find more information in our advanced C++ tutorial coming soon!

Submission

Submit your GitHub link and commit ID to the "Lab 1: Setup & C++" assignment on Gradescope, then get checked off by a TA at hours.

Reference the GitHub + Gradescope Guide here.

You will not get credit for having completed the lab, unless you get it checked off!