Hamburger Icon
  • Labs icon Lab
  • Core Tech
Labs

Guided: Build a CLI Bookmark Manager in C

In this lab, you will build a Command Line Interface (CLI) bookmark manager with C , leveraging the C 's Standard Library, using file I/O for data persistence, and applying object-oriented programming principles.

Labs

Path Info

Level
Clock icon Intermediate
Duration
Clock icon 1h 17m
Published
Clock icon Feb 02, 2024

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Introduction

    Welcome to the lab Guided: Build a CLI Bookmark Manager in C++.

    In this lab, you'll develop a Command Line Interface (CLI) bookmark manager that supports four commands: add, list, remove, and launch.

    You can add three types of bookmarks: Academic, Media, and Web. Common fields for all bookmark types include the URL, title, and tags. Academic bookmarks require additional fields for author(s) and publication dates, while Media bookmarks need details about the media type and duration. The URL is the only required field and will be validated.

    The remove and launch commands will need the bookmark's ID. The list command provides a detailed view, allowing you to directly remove or launch bookmarks.

    Bookmarks are stored in a text file (bookmarks.txt), with each field separated by a pipe (|) character. For example:

    web|1|https://google.com|Google|tag1, tag2,
    media|2|https://youtu.be/watch?v=123|Music Video|video tag,|Video|5 min
    academic|3|http://example.com|Academic bookmark|tag1,|Jan 26, 2024|author
    

    Upon launch, the application reads this file and stores the bookmarks in a vector as instances of three classes: WebBookmark, MediaBookmark, and AcademicBookmark, all subclasses of the Bookmark class. The text file is updated whenever a bookmark is added or removed, reflecting the current state of the bookmarks in the vector. ---

    Familiarizing with the Program Structure

    Here's a description of the main files of the application:

    1. src/bookmark/Bookmark.cpp: Implements the base class for all types of bookmarks in the application, containing common attributes and methods.

    2. src/bookmark/WebBookmark.cpp: Implements the WebBookmark class, which is a subclass of the Bookmark class tailored for general web-related bookmarks. This class uses the same attributes defined in the base class, it doesn't declare additional attributes.

    3. src/bookmark/AcademicBookmark.cpp: Implements the AcademicBookmark class, which is a specialized subclass of the Bookmark class for academic-related bookmarks. This class is declared with additional member variables specific to academic bookmarks: publicationDate and authors.

    4. src/bookmark/MediaBookmark.cpp: Implements the MediaBookmark class, which is a subclass of the Bookmark class tailored for media-related bookmarks. This class is declared with additional member variables specific to media bookmarks: mediaType and duration.

    5. src/BookmarkManager.cpp: Implements the BookmarkManager class, which is responsible for managing a collection of bookmarks within the application, serving as the central component for bookmark storage and operations.

    6. src/CLI.cpp: Implements the CLI class, which provides the user interface for interacting with the BookmarkManager class in the application. It handles all interactions with the user, delegating bookmark management tasks to the BookmarkManager class.

    7. src/Main.cpp: Serves as the entry point for the bookmarks command-line application, orchestrating the initialization and execution of the application's user interface and bookmark management components.

    8. src/Utils.cpp: This file defines the Utils class, which contains utility functions that are used throughout the bookmarks application to handle string manipulation and validation tasks.

    Your primary focus will be on the BookmarkManager class and the AcademicBookmark and MediaBookmark subclasses. As you progress, you'll understand how the other files interplay with these classes. Each file, along with its header file in the include directory, is extensively commented to help you understand what they do.

    You can compile and run the existing program using the Run button located at the bottom-right corner of the Terminal. Initially, it will compile with some warnings. It won't load from or save bookmarks to the bookmarks.txt file, and it won't produce functional outputs. However, you can navigate through the menus and options.

    Begin by familiarizing yourself with the setup. When you're ready, dive into the coding process. If you have problems, remember that a solution directory is available for reference or to verify your code.

    Let's get started.

  2. Challenge

    Step 1: Using Inheritance and Overriding Methods

    Understanding Inheritance

    Inheritance is a fundamental concept in object-oriented programming (OOP). It allows a class, referred to as a derived class, to inherit attributes and methods from another class, known as a base class. This enables code reusability and the creation of a more organized and modular code structure.

    In this application, Bookmark serves as the base class, encapsulating properties and behaviors common across all bookmarks, such as id, url, title, and tags. It also includes common methods like printDetails() and saveFields(). Subclasses, such as AcademicBookmark and MediaBookmark, extend the Bookmark class by adding specific attributes. This design allows for more targeted functionality and data handling, tailored to the content type of each bookmark.

    In the file, include/bookmark/Bookmark.hpp, you can see that the Bookmark class defines a pure virtual function getTypeIdentifier() and a virtual function printAdditionalDetails():

    class Bookmark {
    public:
      // ...
      virtual std::string getTypeIdentifier() const = 0;
      // ...
    protected:
      virtual void printAdditionalDetails() const {}
      // ...
    };
    

    Declared with = 0, pure virtual functions make the Bookmark class abstract, preventing it from being instantiated directly. Derived classes must provide implementations for pure virtual functions.

    On the other hand, the printAdditionalDetails() method is meant to be overridden by derived classes that need to print additional information specific to their type, but it's not pure, it has a default implementation that does nothing, allowing derived classes to choose not to override it:

    class Bookmark {
    public:
      // ...
    
    protected:
      virtual void printAdditionalDetails() const {} // Print subclass-specific details.
      // ...
    };
    

    For example, both AcademicBookmark and MediaBookmark override getTypeIdentifier() to return a unique identifier for their respective types. They also override printAdditionalDetails() to print information relevant to their specific types, like publication dates and authors for academic bookmarks.

    The printAdditionalDetails() method is invoked within the printDetails() method, which is defined in src/bookmark/Bookmark.cpp, and is inherited by all the bookmark types. Here's the implementation of printDetails():

    void Bookmark::printDetails() const {
      std::cout << "---------------------\n" 
         << getTypeIdentifier()
         << " Bookmark\n---------------------"
         << "\nID: " << id 
         << "\nTitle: " << title 
         << "\nURL: " << url 
         << "\nTags: " << Utils::tagsToString(tags);
    
      printAdditionalDetails();
    
      std::cout << std::endl;
    }
    

    printDetails() prints the information common to all bookmark types. After that, it executes the printAdditionalDetails() method to print information specific to each bookmark type.

    Notice that WebBookmark does not override printAdditionalDetails(), indicating that web bookmarks do not have additional details beyond those stored in the Bookmark base class. A derived class can choose to use the base class implementation of a method if the base version is adequate, avoiding unnecessary overrides.

    In the next tasks, you'll implement the getTypeIdentifier() and printAdditionalDetails() methods within the AcademicBookmark and MediaBookmark subclasses.

  3. Challenge

    Step 2: Load Bookmarks from the File

    Reading Text Files with std::ifstream

    You can read text files in C++ by using the std::ifstream class from the Standard Library.

    std::ifstream stands for input file stream, and to use it, you need to include the header <fstream> in your program. Here’s an example about how to open and read a text file:

    #include <fstream>
    #include <iostream>
    #include <string>
    
    int main() {
      std::ifstream file;
      file.open("example.txt");
    
      if (file.is_open()) {
        std::string line;
        while (std::getline(file, line)) {
          // Process each line read from the file
          std::cout << line << std::endl;
        }
        file.close(); // Close the file
      } else {
        std::cout << "Unable to open file" << std::endl;
      }
    
    return 0;
    }
    

    Where:

    • std::ifstream file; declares a variable with the name file of type std::ifstream.
    • file.open("example.txt"); attempts to open example.txt for reading.
    • The if (file.is_open()) statement checks if the file was opened successfully before attempting to read from it.
    • The std::getline(file, line) function reads data from file until a newline character is found, storing the text in the line string. This loop continues until all lines are read.
    • Finally, file.close() closes the file.

    You can use if (file) instead of if (file.is_open()) to check if a file stream was successfully opened:

    std::ifstream file;
    file.open("example.txt");
    
    if (file) {
      // Process the file...
    } else {
      std::cout << "Unable to open file" << std::endl;
    }
    

    This is because std::ifstream (and other standard stream types like std::ofstream and std::fstream) can be implicitly converted to a boolean. It returns true if the stream is ready for I/O operations (like reading or writing) and false if not (for example, if the file failed to open).

    Although it is considered good practice, you don't have to explicitly close the file by calling file.close(). std::ifstream objects automatically close the file when they go out of scope (when the variable file is destroyed).

    In the application, the constructor of the BookmarkManager class calls the loadBookmarksFromFile method when the program is launched:

    BookmarkManager::BookmarkManager() {
      loadBookmarksFromFile(BOOKMARKS_FILENAME);
    }
    

    This method is responsible for loading bookmarks from a file and managing their IDs. Initially, it clears any existing bookmarks in the bookmarks vector, resets the ID counter, and then attempts to load bookmarks from a file. If a line represents a valid bookmark, a Bookmark object is created and added to the vector, tracking the highest bookmark ID. At the end, the method sets the next available ID to one more than the highest ID found in the file.

    In the next tasks, you'll complete the implementation of the loadBookmarksFromFile method. ---

    Using std::istringstream to Read Lines From a File

    The std::istringstream class, also part of the C++ Standard Library, is designed for performing input operations on strings. This class enables treating a std::string object as a stream, allowing you to extract values as if you were reading from std::cin or other input streams.

    To use std::istringstream, include the <sstream> header:

    #include <sstream>
    

    This allows you to declare an std::istringstream object and then set its contents using the .str() method:

    std::istringstream linestream;
    linestream.str(line);
    

    Initially, linestream is declared as an empty stream. Then, the .str(line) method is used to populate the stream with the content of the line string.

    This way, data can be extracted using the extraction operator (>>) or functions such as std::getline(). The latter is particularly useful when you want to parse a string by delimiters:

    std::string value;
    std::getline(linestream, value, FIELD_SEPARATOR);
    

    The above line of code reads from linestream up to the FIELD_SEPARATOR, a delimiter that can be a character such as a comma, space, or any other character used to separate data fields in a string. The extracted substring, which does not include the delimiter, is stored in value.

    In the bookmarks.txt file, each line represents a bookmark, formatted as follows:

    [BOOKMARK_TYPE]|[ID]|[URL]|[TITLE]|[TAG1, TAG2, ...]|[EXTRA_FIELD1]|[EXTRA_FIELD2]|...
    

    For the next task, your objective is to extract the initial piece of information from each line, specifically, the bookmark's type. ---

    Understanding the Bookmark::createBookmark Method

    Each bookmark type stores different information in the file. Once you have determined the type of bookmark a line in the file represents, you can create the correct bookmark instance.

    The Bookmark::createBookmark method is a static factory method used to create instances of derived Bookmark types (AcademicBookmark, MediaBookmark, WebBookmark) based on a type identifier. The method is static because it is called on the class itself, rather than on an instance of the class. This allows for the creation of objects without needing an existing object, which is essential for factory methods that serve as object creation utilities. Here's its implementation:

    std::unique_ptr<Bookmark> Bookmark::createBookmark(const std::string& type, std::istringstream& linestream, char separator) {
      std::unique_ptr<Bookmark> bookmark = nullptr;
        
      if (type == WEB_TYPE_IDENTIFIER) {
        bookmark =  WebBookmark::createFromFile(linestream, separator);
      } else if (type == MEDIA_TYPE_IDENTIFIER) {
        bookmark =  MediaBookmark::createFromFile(linestream, separator);
      } else if (type == ACADEMIC_TYPE_IDENTIFIER) {
        bookmark =  AcademicBookmark::createFromFile(linestream, separator);
      }
    
      return bookmark;  // Return nullptr if type does not match any known subclass
    }
    

    This method accepts a type identifier, an input stream, and a separator character as arguments, to determine the type of bookmark to create based on the identifier, and then delegates the creation process to the appropriate subclass's createFromFile method.

    Each Bookmark subclass has its own createFromFile static method. These methods handle the creation of their respective types from an input stream, allowing for the parsing of subclass-specific fields. They are static because they do not operate on a particular instance of the class but are used to generate new instances. As an example, here's the implementation for WebBookmark:

    std::unique_ptr<Bookmark> WebBookmark::createFromFile(std::istringstream& linestream, char separator) {
      auto bookmark = Bookmark::createBookmarkFromStream<WebBookmark>(linestream, separator);
        
      // No additional fields to fill in
    
      return bookmark;
    }
    

    These methods use the template method Bookmark::createBookmarkFromStream<T> to create a new instance of the subclass. A template factory method, using a template parameter (T), generalizes object creation across different types. This allows the method to be used with different types while providing a common implementation.

    Template methods are often defined in header files. In the include/bookmark/Bookmark.hpp file, you can see the definition of the createBookmarkFromStream method:

    template<typename T>
    static std::unique_ptr<Bookmark> createBookmarkFromStream(std::istringstream& stream, char separator) {
      // Declare variables to store data extracted from the line stream 
      std::string idStr, url, title, tagsConcat;
    
      // Extract common data
      std::getline(stream, idStr, separator); // Extract id as string
      std::getline(stream, url, separator);   // Extract URL
      std::getline(stream, title, separator); // Extract title
      std::getline(stream, tagsConcat, separator); // Extract concatenated tags
    
      auto tags = Utils::splitTags(tagsConcat); // Split the tag string into a vector
      int id = std::stoi(idStr); // Convert id string to integer
    
      // Construct and return concrete T instance 
      return std::unique_ptr<T>(new T(id, url, title, tags));
    }
    

    This method reads and parses common fields from a stream and creates an instance of the type T, where T is a subclass of Bookmark, returning a pointer (std::unique_ptr<Bookmark>) to the newly created object. This approach abstracts and reuses the common parsing logic across different bookmark types, effectively reducing code duplication.

    WebBookmark::createFromFile only needs to call the template method createBookmarkFromStream because web bookmarks do not have additional fields beyond what the Bookmark base class already handles. In contrast, for AcademicBookmark and MediaBookmark you'll need to extract the additional fields they define.

  4. Challenge

    Step 3: Saving Bookmarks to a File

    Writing to Files with std::ofstream

    std::ofstream is a class provided by the C++ Standard Library that allows you to write data to files.

    std::ofstream stands for output file stream, and to use it, you must include the <fstream> header in your program. This way, you can declare an std::ofstream object first and then open the file in a separate step:

    #include <fstream>
    
    // ...
    
    std::ofstream file;
    file.open(FILENAME, std::ios::app);
    

    In the above example, the open method is called on the ofstream object, file, with the filename and mode provided as argument.

    When you open a file for writing using std::ofstream, you have the option to specify the mode in which the file should be opened. The mode determines how the file will be used and what happens to the file's existing contents. Here are some modes you can use:

    1. std::ios::out: This flag is used to specify that the file stream is opened for writing, so it's the default mode for std::ofstream. If you open a file with std::ios::out, and the file already exists, its contents will not be destroyed until you actually write to the file, starting from the beginning. If the file does not exist, it will be created.

    2. std::ios::app: This flag is used to specify that all output operations are performed at the end of the file, appending the new data to the existing content. If you open a file with std::ios::app, the file's write position will always be at the end of the file. This means you cannot overwrite existing content in the file; you can only add to it.

    3. std::ios::trunc: This flag is used to specify that if the file already exists, its contents will be truncated, meaning all the existing data in the file will be removed upon opening the file.

    You can combine more than one mode using the bitwise OR operator (|).

    If you're wondering what's the difference between ios::out and ios::trunc, using std::ios::trunc alone does not imply writing or reading mode; it only specifies the behavior regarding the file's contents. However, when used with std::ofstream, since std::ios::out is the default mode, std::ios::trunc effectively deletes all the content of a file before writing to it (as if opening the file with std::ios::out | std::ios::trunc).

    In the next task, you'll modify the BookmarkManager::saveBookmarksToFile method to open the bookmarks.txt file for writing. ---

    Understanding Bookmark::saveFields and Subclass Implementation

    The Bookmark::saveFields method in the Bookmark base class is designed to save the common fields of a bookmark (like id, url, title, and tags) to a file:

    void Bookmark::saveFields(std::ostream& file, char separator) const {
      file << getTypeIdentifier() << separator
           << id << separator
           << url << separator
           << title << separator
           << Utils::tagsToString(tags);
    
      saveAdditionalFields(file, separator);
    }
    

    This method is inherited by all subclasses (AcademicBookmark, MediaBookmark, and WebBookmark), allowing them to leverage the same functionality for saving common fields and extend it by overriding the saveAdditionalFields method to save subclass-specific fields.

    The syntax file << [VARIABLE] << separator is used for writing data to the output stream. The << operator is overloaded for file streams and various data types, allowing for easy writing of data to files. The separator is used to delimit fields in the file, ensuring that when the bookmarks are loaded back, the fields can be correctly parsed.

    The WebBookmark class does not override the saveAdditionalFields method, since it does not have any fields beyond what is already defined in the Bookmark base class.

    In contrast, AcademicBookmark and MediaBookmark need to implement this method to handle any fields that are specific to those bookmark types. You'll do this in the next tasks.

  5. Challenge

    Step 4: Finding Bookmarks

    Understanding std::vector

    std::vector is a sequence container in the C++ Standard Library that represents a dynamically resizable array.

    In the application, the bookmarks are stored in a std::vector where each element is a unique_ptr that points to an object of a derived class of Bookmark (like AcademicBookmark, MediaBookmark, or WebBookmark). You can see the definition of this vector in the file include/BookmarkManager.hpp:

    class BookmarkManager {
    private:
      std::vector<std::unique_ptr<Bookmark>> bookmarks;
      // Rest of the class...
    

    std::unique_ptr is a smart pointer that manages another object through a pointer and automatically disposes of that object when the unique_ptr goes out of scope. This ensures automatic resource management, including memory deallocation, to prevent memory leaks.

    You can iterate over elements in a std::vector using a range-based for loop:

    std::vector<std::unique_ptr<Object>> container;
    
    for (const auto& element : container) {
      // Your code here
    }
    

    In this loop:

    • element refers to the current element in the container.
    • auto allows the compiler to deduce the type of element.
    • const auto& accesses each element by reference to avoid unnecessary copies and indicates that elements are not modified.

    To call methods on the objects pointed by a unique_ptr, use the -> operator:

    element->aField;
    element->aMethod();
    

    Alternatively, you can use a traditional for loop with an index:

    std::vector<std::unique_ptr<Object>> container;
    
    for (int i = 0; i < static_cast<int>(container.size()); ++i) {
      // Your code here
    }
    

    Here:

    • i is the loop index, starting from 0.
    • The loop runs while i is less than the container's size.
    • static_cast<int>(container.size()) converts the size (of type std::size_t) to int to match the type of i.

    Inside the loop, you can access the element at the i-th position in the container using container[i]. To access the members of the Bookmark object pointed to by a unique_ptr, use the -> operator:

    container[i]->aField;
    container[i]->aMethod();
    

    The findBookmarkIndexById method in BookmarkManager is used by other methods in the class to search for a bookmark with a matching ID and return its index in the bookmarks vector. You'll complete the implementation of findBookmarkIndexById in the following task.

  6. Challenge

    Step 5: Adding and Removing Bookmarks

    Adding Elements to a Vector

    To append an element to a std::vector, use the push_back method, passing the new element as an argument:

    std::vector<int> myVector;
    myVector.push_back(42);
    

    When dealing with std::unique_ptr, a smart pointer that ensures exclusive ownership of an object, special handling is required. Since a unique_ptr cannot be copied (to avoid multiple ownership issues), it must be moved using std::move. This function transfers the ownership of the object to another unique_ptr, leaving the original unique_ptr in a null state.

    Consider the following example:

    std::vector<std::unique_ptr<MyClass>> myVector;
    std::unique_ptr<MyClass> myPtr;
    

    To add myPtr to myVector, you would do:

    myVector.push_back(std::move(myPtr));
    

    After this, myPtr will no longer own the object (it will be null), and myVector will take ownership of the unique_ptr.

    Understanding this is important for your next task, where you will implement a method to add a new bookmark to a vector of bookmarks. ---

    Removing Elements from a Vector

    The erase method is used to remove elements from a std::vector. It can delete either a single element or a range of elements.

    To remove a single element, provide erase with an iterator to the target element. The following example removes the element at a specific index:

    vec.erase(vec.begin() + index);
    

    Here vec.begin() + index calculates an iterator to the index-th element (considering 0-based indexing), which is then removed.

    For removing a range of elements, erase requires two iterators: the start and the end (exclusive) of the range. For example:

    vec.erase(vec.begin() + start, vec.begin() + end);
    

    This removes elements from the start position up to, but not including, the end position.

    In both cases, the begin method returns an iterator pointing to the first element of the vector. Since iterators support arithmetic operations, adding an index to begin() calculates the position of the element to be erased.

    For example, suppose you have this vector:

    std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    

    To remove the element at position index, say index = 3, use the erase method:

    int index = 3; // Remove the fourth element (0-based index)
    vec.erase(vec.begin() + index);
    

    After this operation, the vector looks like this:

    {1, 2, 3, 5, 6, 7, 8, 9, 10}
    

    The fourth element (4 in this case) is removed, and the remaining elements are shifted left.

    Similarly, to remove a range from start = 4 to end = 7, the erase method can be used as follows:

    int start = 4; // Starting from the fifth element
    int end = 7;   // Up to (but not including) the eighth element
    vec.erase(vec.begin() + start, vec.begin() + end);
    

    This modifies the vector to:

    {1, 2, 3, 4, 8, 9, 10}
    

    Removing the elements 5, 6, and 7.

    In the next task, you will complete the implementation of a method to remove a bookmark from the vector containing all bookmarks.

  7. Challenge

    Step 6: Launching Bookmarks

    Launching URLs and Using std::async

    Launching a URL in the default web browser is done by executing a system command, which varies depending on the operating system:

    • Windows: Use the start command followed by the URL. Example: start http://example.com.
    • macOS: Use the open command followed by the URL. Example: open http://example.com.
    • Linux: Use the xdg-open command followed by the URL. Example: xdg-open http://example.com.

    In C++, you can execute these commands using std::system, a function from the C++ Standard Library that executes a system command as if it were run from the command line:

    int value = std::system("open http://example.com");
    

    This function:

    • Takes a single const char* argument, the command to be executed, as string.
    • Returns an integer value, which is the command's exit status. A return value of 0 typically indicates success.

    To ensure the correct command is used on each platform, you can use conditional compilation directives:

    #ifdef __APPLE__
      int value = std::system("open http://example.com");
    #elif _WIN32
      int value = std::system("start http://example.com");
    #else
      int value = std::system("xdg-open http://example.com");
    #endif
    

    These directives check for predefined macros that indicate the operating system and execute the appropriate command for that operating system.

    However, note that executing a system command to open a URL can block the executing thread until the command is completed. To prevent blocking, especially in a GUI application or a service handling multiple requests, it's important to make such calls asynchronously.

    For this, you might consider one of the following approaches:

    • Detached Thread: One approach is to run the task in a detached thread. This means the thread runs independently and no return value is provided. You can achieve this by directly creating a std::thread and detaching it:

      std::thread([](const std::string& url) {
        // System command to open URL
      }, bookmark.url).detach();
      

      This approach fully decouples the launched task from the main thread, avoiding any blocking. However, it also means you lose control over the task once it's detached, so you can't wait for its completion or retrieve its result.

    • std::future Object: If you need to ensure that the tasks complete and you might want to check their status or results later, you can utilize a std::future object by using std::async:

      std::async(std::launch::async, [](const std::string& url) {
        // System command to open URL
      }, bookmark.url);
      

      This approach allows you to query or wait for the tasks at a more appropriate time, for example, when the program is idle or before it exits.

    While a detached thread could run the command asynchronously, std::async is preferred when the command's return value matters. std::async runs a function asynchronously (potentially in a new thread) and returns a std::future object that holds the result.

    When you call std::async, you have the option to specify a launch policy:

    • std::launch::async: This policy indicates that the task should run on a new thread.
    • std::launch::deferred: This policy means that the task is executed on the calling thread the first time its result is requested via the std::future::get method.

    So, to use std::async, include the <future> header and call std::async with std::launch::async to enforce asynchronous execution, followed by the function and its arguments:

    #include <future>
    
    auto future = std::async(std::launch::async, []() -> int {
      // Your command execution logic here
      return std::system("open http://example.com");
    });
    

    This creates a future that holds the result of the std::system call, allowing the program to continue execution without waiting for the command to complete.

    In the BookmarkManager::launchBookmark method, you'll use std::async with the std::launch::async policy to launch a bookmark URL asynchronously. ---

    Managing std::future Objects

    The destructor of the std::future object returned by std::async blocks until the associated task completes if launched asynchronously (using std::launch::async). This ensures that a task is properly completed before the std::future is destroyed.

    If the std::future is not stored, for instance in a vector, it's destroyed at the end of the expression scope. This can lead to the main thread blocking until the task completes, potentially negating the benefits of the asynchronous execution. A solution is to store the std::future objects in a container, enabling you to check the completion status of the tasks later, such as when the program is idle or before it exits.

    The application is only interested in the result of the system call and don't need a reference to the std::future object for later, so storing the std::future object isn't necessary.

    You can use the std::future object to retrieve the return value of the std::system call. Remember:

    • A return value of 0 usually indicates success.
    • A non-zero return value suggests an error or the command's exit status, which varies depending on the system and command executed.

    To do this, use the future::get method. For example:

    if (fut.valid()) { // Check if the future is valid
      int exitStatus = fut.get(); // Retrieve the command's exit status
      if (exitStatus == 0) {
        std::cout << "Command executed successfully." << std::endl;
      } else {
        std::cout << "Command failed with exit status: " << exitStatus << std::endl;
      }
    }
    

    However, be careful with future::get, as it blocks until the task completes. If the system command takes significant time to execute, it could impact the application's responsiveness. In such cases, consider using std::future::wait_for with a zero timeout to check if the task is completed without blocking:

    if (fut.valid() && fut.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
      int exitStatus = fut.get();
      // ...
    }
    

    In this code:

    • valid() checks if the std::future is associated with a shared state.
    • wait_for() waits for a specified duration for the operation to complete. It returns an enum of type std::future_status, indicating the state of the operation.
    • Once you confirm the future is valid and ready, use get() to retrieve the result. get() can only be called once for each std::future, consuming the shared state.

    This approach uses the std::future object immediately to wait for task completion and retrieve results. After checking the result, the std::future object goes out of scope and is destroyed automatically.

    In the next task, you'll use fut.wait_for with a one-second timeout to prevent indefinite blocking when launching a bookmark. If the task is ready within this time, it proceeds to get the result with future.get(). If the task isn't ready (the wait_for call times out), it logs a timeout message and returns false.

    This approach gives the application a chance to avoid blocking for an extended period if the system command takes too long, but it also introduces a potential issue where a command might not complete within the timeout, leading to a false indication of failure. In a real-world application, you'll need to decide how critical the completion of a command is and handle timeouts accordingly.

  8. Challenge

    Conclusion

    Congratulations on successfully completing this Code Lab!

    To compile and run your program, you'll need to use the Run button. This is located at the bottom-right corner of the Terminal. Here's how to proceed:

    1. Compilation: Clicking Run will compile all the files in the src directory into an executable named BookmarkManager.

    2. Running the Program: After compilation, the program will automatically execute using the command:

      ./BookmarkManager
      

      There is a bookmarks.txt file containing some bookmarks for you to try. Follow the prompts in the menu to add, list, remove, or launch bookmarks. However, please note that the launch functionality will not work in this environment due to its restrictions; it will only work on your local machine. You can review the launch.log file to see the errors generated when attempting to use the launch command. ---

    Extending the Program

    Consider exploring these ideas to further enhance your skills and expand the capabilities of the program:

    1. Separate file I/O operations: Create a new class, for example, BookmarkFileHandler, to handle all file-related operations. This will not only make the BookmarkManager cleaner but also adheres to the Single Responsibility Principle by delegating file operations to a dedicated class.

    2. Use a database instead of a file: Consider using a database instead of a text file. Databases can manage data more effectively and provide powerful query languages (such as SQL) for complex searches, filtering, and data aggregation. The previous change, separating file I/O operations, facilitates an easier transition to a database.

    3. Create a command interface or class: Following the Command design pattern, consider defining a Command abstract base class with an execute() method. Each command (add, list, remove, launch, and help) could be a derived class. This approach means adding new commands would not require extensive modifications to the CLI class.

    4. Implement more functionality: Consider adding more functionalities, such as searching by title or tags, and editing a bookmark. The previous changes will facilitate the implementation of such features.

    5. Improved error handling: For simplicity, the current version ignores string to integer conversion errors when loading the bookmarks file. However, these operations, as well as I/O operations, could throw (custom) exceptions. Ensure to include try-catch blocks around these operations to gracefully handle potential exceptions. ---

    Related Courses on Pluralsight's Library

    If you're interested in further honing your C++ skills or exploring more topics, Pluralsight offers several excellent courses in the following paths:

    These courses cover many aspects of C++ programming. Check them out to continue your learning journey in C++.

Esteban Herrera has more than twelve years of experience in the software development industry. Having worked in many roles and projects, he has found his passion in programming with Java and JavaScript. Nowadays, he spends all his time learning new things, writing articles, teaching programming, and enjoying his kids.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.