Learn C++
Search textbook...
Introduces the C++ type system, and how to construct your own types using structs.
The type of a variable in C++ is the "category" of that variable, or in other words the kind of object that variable represents. C++ comes with many built-in types, such as int, double, bool, std::string, size_t, to name a few. C++ enforces the appropriate use of variable types through its type system. In particular, C++ is a statically typed language, meaning that:
To see what that means, take a look at this snippet of C++ code:
Notice that each variable (a, b, d), parameter (c), and function return type (foo) is required to specify a type, and that attempting to change the existing type of a variable (for example, assigning d from an int to a std::string) as shown in the highlighted region, causes a compiler error.
voidis a special type indicating that a function has no return type.
Compare this code to its equivalent in Python, a dynamically typed language:
Notice that types are omitted, and one can change the type of the variable dynamically from an int to a str without issue.
Unlike Python, C++ allows two functions to have the same name, as long as they differ in number or type of parameters. This is known as function overloading. For example, if we declare the functions
Then calling
func(2)will return5.0, whereasfunc(2.0)will return6.0(notice the parameter was anintin the first case, and adoublein the second).
Static typing offers a number of benefits to performance and readability. In particular, it gives the compiler additional information about variables, allowing it to allocate memory for these variables more efficiently. The compiler might also be able to make additional performance optimizations in the resulting machine code if it can depend on the values having a certain structure in memory, and eliminate the need for runtime checks on the type of an object that a dynamically typed language may suffer.
In larger organizations and codebases, static typing also makes code easier to understand and reason about. Knowing that an object is an int or a std::string tells you what operations are valid for that object (e.g. it makes sense to multiply two ints, but not two std::strings) and where it can be used (which functions it can be passed to). In the case of C++, these restrictions are enforced by the compiler when code is compiled.
For example, the following Python program runs but encounters a runtime error:
However, it fails to compile in C++:
This is a simple example, but in a larger system with many interlocking parts, having a robust type-system makes it possible to catch these kinds of errors before the program has a chance to run.
Structs are a way to extend the type system by bundling multiple values together into one object.
Imagine that you are working for a university IT department that manufactures ID cards for enrolled students. The university wants to automate the process for printing new student ID cards using a C++ interface, and asks you to write the following function:
However, an ID card has more than one piece of information that you want to keep track of. For example, we want to track the name, ID number, and email of the associated student. How can we return all three pieces of information from one function (what do we replace the ????? with in the code above)?
One way to accomplish this is to declare a struct that represents the combination of all three values:
Notice that the struct above is composed of three fields, each looking somewhat like a variable, with a name and a type. We could then implement the printIDCard function to initialize and return an IDCard like so:
This function always returns the same ID—a more realistic implementation would probably return different IDs, depending on a variety of factors (already in-use ID, name of the student whose ID should be generated, etc.)
This is a standard, C-style way of initializing structs. A more modern way might be to use uniform initialization, which is covered in the next chapter:
Notice that the order that the fields are initialized in this format depends on the order that they were declared in the IDCard definition. We can simplify this even further like so:
std::pairOften times, we want to refer to a pair of values without creating an entirely new struct. For example, suppose we implement a function that finds the first and last indices of a character in a string:
std::string::nposis a special value that refers to a non-existent position in a string. Read more in the C++ documentation.
This code would work great! But suppose that this is the only time we ever used the CharacterIndices. Rather than spin-up a new type, it would be more direct to use std::pair instead:
This code functions equivalently, but returns the built-in type std::pair instead. The fields of std::pair are first and second:
std::pairis technically not a type, but a template. When using pair, we must list the types offirstandsecondinside the<>characters, e.g.std::pair<std::string, size_t>. Templates will be discussed extensively in a later chapter.
Because C++ is a statically-typed language, the types of every variable, parameter, and function return type must be known at compile time. While this affords us many perks (as discussed above), writing out long type names can become inconvenient. To counteract this, modern C++ offers two mechanisms to make typing easier.
Consider the following function signature, which computes the solution to the quadratic equation as a std::pair<bool, std::pair<double, double>>. Note that the bool field indicates whether or not the equation had a solution.
In modern C++, it would make more sense to return a
std::optional<std::pair<double, double>>here.std::optionalis discussed in a later chapter.
usingTo avoid the hassle of writing a long type name like std::pair<bool, std::pair<double, double>>, we could create a type alias for that type with the using keyword. If you have used C before, this is identical to a typedef:
QuadraticSolution has the benefit of being a shorter type name, but also might be a bit clearer. Rather than being a seemingly-arbitrary pair object, we know that this specifically refers to a solution to a quadratic equation. In all other aspects, however, the code would be identical if we had written the type out by hand.
autoIn other cases, we'd prefer not to have to worry about the types at all. In these situations, we can write the type as auto to have the compiler infer a variable, parameter, or function return type from the context in which it occurs. For example:
To be clear, this code is still statically typed! It would be invalid to try to assign anything other than a std::pair<bool, std::pair<double, double>> to soln, and this code is exactly the same as if we had written std::optional<std::pair<double, double>> in place of auto. However, we let the compiler do the heavy lifting for us: since it knows what the return type of solveQuadratic is, it infers (or deduces) the type of soln.
One area you will often see this used is in range-based for loops. For example:
Because we are iterating through a std::vector<int>, the compiler is smart enough to infer that the type of elem is int.
The compiler can even infer the return type of a function, so long as it is unambigiously clear what that return type is:
In this case, the compiler examines the type of the returned object (20) to derive the return type of smeagol as int. However, in the following example, the compiler cannot deduce if the return type is, e.g. a std::pair<double, double>, a std::vector<double>, or some user-defined struct containing two double fields:
To fix this issue, we can explicitly specify the return type:
In C++, you can even list the type of a parameter as
auto. As we will learn in the Templates chapter, this syntax is exactly identical to creating a function that is templated on theautoargument, meaning that theautotype will be inferred with whatever type is passed to the function when it is called.