8 Matching Annotations
  1. Dec 2024
    1. The Containers library is a part of the C++ standard library that contains various class types that implement some common types of containers. A class type that implements a container is sometimes called a container class. The full list of containers in the Containers library is documented here.In C++, the definition of “container” is narrower than the general programming definition. Only the class types in the Containers library are considered to be containers in C++. We will use the term “container” when talking about containers in general, and “container class” when talking specifically about the container class types that are part of the Containers library.

      The following types are containers under the general programming definition, but are not considered to be containers by the C++ standard: * C-style arrays * std::string * std::vector<bool>

      To be a container in C++, the container must implement all of the requirements listed here. Note that these requirements include the implementation of certain member functions -- this implies that C++ containers must be class types! The types listed above do not implement all of these requirements. However, because std::string and std::vector<bool> implement most of the requirements, they behave like containers in most circumstances. As a result, they are sometimes called “pseudo-containers”.

    2. The elements of a container do not have their own names, so that the container can have as many elements as we want without having to give each element a unique name.

      Each container provides some method to access these elements, but how depends on the specific type of container.

    1. Try blocks and catch blocks work together -- a try block detects any exceptions that are thrown by statements within the try block, and routes them to a catch block with a matching type for handling. A try block must have at least one catch block immediately following it, but may have multiple catch blocks listed in sequence.Once an exception has been caught by the try block and routed to a matching catch block for handling, the exception is considered handled. After the matching catch block executes, execution then resumes as normal, starting with the first statement after the last catch block.Catch parameters work just like function parameters, with the parameter being available within the subsequent catch block. Exceptions of fundamental types can be caught by value, but exceptions of non-fundamental types should be caught by const reference to avoid making an unnecessary copy (and, in some cases, to prevent slicing).

      If the parameter is not going to be used in the catch block, the variable name can be omitted

    1. One of the most common ways to handle potential errors is via return codes.

      The primary virtue of this approach is that it is extremely simple. However, using return codes has a number of drawbacks which can quickly become apparent when used in non-trivial cases:

      First, return values can be cryptic -- if a function returns -1, is it trying to indicate an error, or is that actually a valid return value? It’s often hard to tell without digging into the guts of the function or consulting documentation.

      Second, functions can only return one value, so what happens when you need to return both a function result and a possible error code? Consider the following function:

      double divide(int x, int y) { return static_cast<double>(x)/y; } This function is in desperate need of some error handling, because it will crash if the user passes in 0 for parameter y. However, it also needs to return the result of x/y. How can it do both? The most common answer is that either the result or the error handling will have to be passed back as a reference parameter, which makes for ugly code that is less convenient to use. For example:

      include <iostream>

      double divide(int x, int y, bool& outSuccess) { if (y == 0) { outSuccess = false; return 0.0; }

      outSuccess = true;
      return static_cast<double>(x)/y;
      

      }

      int main() { bool success {}; // we must now pass in a bool value to see if the call was successful double result { divide(5, 3, success) };

      if (!success) // and check it before we use the result
          std::cerr << "An error occurred" << std::endl;
      else
          std::cout << "The answer is " << result << '\n';
      

      } Third, in sequences of code where many things can go wrong, error codes have to be checked constantly. Consider the following snippet of code that involves parsing a text file for values that are supposed to be there:

      std::ifstream setupIni { "setup.ini" }; // open setup.ini for reading // If the file couldn't be opened (e.g. because it was missing) return some error enum if (!setupIni) return ERROR_OPENING_FILE;

      // Now read a bunch of values from a file if (!readIntegerFromFile(setupIni, m_firstParameter)) // try to read an integer from the file return ERROR_READING_VALUE; // Return enum value indicating value couldn't be read

      if (!readDoubleFromFile(setupIni, m_secondParameter)) // try to read a double from the file return ERROR_READING_VALUE;

      if (!readFloatFromFile(setupIni, m_thirdParameter)) // try to read a float from the file return ERROR_READING_VALUE; We haven’t covered file access yet, so don’t worry if you don’t understand how the above works -- just note the fact that every call requires an error-check and return back to the caller. Now imagine if there were twenty parameters of differing types -- you’re essentially checking for an error and returning ERROR_READING_VALUE twenty times! All of this error checking and returning values makes determining what the function is trying to do much harder to discern.

      Fourth, return codes do not mix with constructors very well. What happens if you’re creating an object and something inside the constructor goes catastrophically wrong? Constructors have no return type to pass back a status indicator, and passing one back via a reference parameter is messy and must be explicitly checked. Furthermore, even if you do this, the object will still be created and then has to be dealt with or disposed of.

      Finally, when an error code is returned to the caller, the caller may not always be equipped to handle the error. If the caller doesn’t want to handle the error, it either has to ignore it (in which case it will be lost forever), or return the error up the stack to the function that called it. This can be messy and lead to many of the same issues noted above.

    2. Exception handling provides a mechanism to decouple handling of errors or other exceptional circumstances from the typical control flow of your code. This allows more freedom to handle errors when and how ever is most useful for a given situation, alleviating most (if not all) of the messiness that return codes cause.

      Using return codes is simpler but ends up linked to the control flow, constraining both how the code is laid out, and how errors can be reasonably handled.

    1. Because there are multiple steps involved, the term building is often used to refer to the full process of converting source code files into an executable that can be run. A specific executable produced as the result of building is sometimes called a build.

      For complex projects, build automation tools (such as make or build2) are often used to help automate the process of building programs and running automated tests. While such tools are powerful and flexible, because they are not part of the C++ core language, nor do you need to use them to proceed, we’ll not discuss them as part of this tutorial series.

    1. Because source code is written using ASCII characters, programming languages use a certain amount of ASCII art to represent mathematical concepts. For example, ≠ is not part of the ASCII character set, so programming languages typically use != to represent mathematical inequality instead.

      Some programming fonts, such as Fira Code, use ligatures to combine such “art” back into a single character. For example, instead of displaying !=, Fira Code will display ≠ (using the same width as the two-character version). Some people find this easier to read, others prefer sticking with a more literal interpretation of the underlying characters.

    2. Various studies have shown that on complex software systems, only 10-40% of a programmer’s time is actually spent writing the initial program. The other 60-90% is spent on maintenance, which can consist of debugging (removing bugs), updates to cope with changes in the environment (e.g. to run on a new OS version), enhancements (minor changes to improve usability or capability), or internal improvements (to increase reliability or maintainability)1.

      Consequently, it’s worth your time to spend a little extra time up front (before you start coding) thinking about the best way to tackle a problem, what assumptions you are making, and how you might plan for the future, in order to save yourself a lot of time and trouble down the road.