- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
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.
Lab Info
Table of Contents
-
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|authorUpon launch, the application reads this file and stores the bookmarks in a vector as instances of three classes:
WebBookmark,MediaBookmark, andAcademicBookmark, all subclasses of theBookmarkclass. 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:
-
src/bookmark/Bookmark.cpp: Implements the base class for all types of bookmarks in the application, containing common attributes and methods. -
src/bookmark/WebBookmark.cpp: Implements theWebBookmarkclass, which is a subclass of theBookmarkclass tailored for general web-related bookmarks. This class uses the same attributes defined in the base class, it doesn't declare additional attributes. -
src/bookmark/AcademicBookmark.cpp: Implements theAcademicBookmarkclass, which is a specialized subclass of theBookmarkclass for academic-related bookmarks. This class is declared with additional member variables specific to academic bookmarks:publicationDateandauthors. -
src/bookmark/MediaBookmark.cpp: Implements theMediaBookmarkclass, which is a subclass of theBookmarkclass tailored for media-related bookmarks. This class is declared with additional member variables specific to media bookmarks:mediaTypeandduration. -
src/BookmarkManager.cpp: Implements theBookmarkManagerclass, which is responsible for managing a collection of bookmarks within the application, serving as the central component for bookmark storage and operations. -
src/CLI.cpp: Implements theCLIclass, which provides the user interface for interacting with theBookmarkManagerclass in the application. It handles all interactions with the user, delegating bookmark management tasks to theBookmarkManagerclass. -
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. -
src/Utils.cpp: This file defines theUtilsclass, 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
BookmarkManagerclass and theAcademicBookmarkandMediaBookmarksubclasses. As you progress, you'll understand how the other files interplay with these classes. Each file, along with its header file in theincludedirectory, 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.txtfile, 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
solutiondirectory is available for reference or to verify your code.Let's get started.
-
-
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,
Bookmarkserves as the base class, encapsulating properties and behaviors common across all bookmarks, such asid,url,title, andtags. It also includes common methods likeprintDetails()andsaveFields(). Subclasses, such asAcademicBookmarkandMediaBookmark, extend theBookmarkclass 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 theBookmarkclass defines a pure virtual functiongetTypeIdentifier()and a virtual functionprintAdditionalDetails():class Bookmark { public: // ... virtual std::string getTypeIdentifier() const = 0; // ... protected: virtual void printAdditionalDetails() const {} // ... };Declared with
= 0, pure virtual functions make theBookmarkclass 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
AcademicBookmarkandMediaBookmarkoverridegetTypeIdentifier()to return a unique identifier for their respective types. They also overrideprintAdditionalDetails()to print information relevant to their specific types, like publication dates and authors for academic bookmarks.The
printAdditionalDetails()method is invoked within theprintDetails()method, which is defined insrc/bookmark/Bookmark.cpp, and is inherited by all the bookmark types. Here's the implementation ofprintDetails():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 theprintAdditionalDetails()method to print information specific to each bookmark type.Notice that
WebBookmarkdoes not overrideprintAdditionalDetails(), indicating that web bookmarks do not have additional details beyond those stored in theBookmarkbase 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()andprintAdditionalDetails()methods within theAcademicBookmarkandMediaBookmarksubclasses. -
Challenge
Step 2: Load Bookmarks from the File
Reading Text Files with
std::ifstreamYou can read text files in C++ by using the
std::ifstreamclass from the Standard Library.std::ifstreamstands 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 namefileof typestd::ifstream.file.open("example.txt");attempts to openexample.txtfor 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 fromfileuntil a newline character is found, storing the text in thelinestring. This loop continues until all lines are read. - Finally,
file.close()closes the file.
You can use
if (file)instead ofif (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 likestd::ofstreamandstd::fstream) can be implicitly converted to aboolean. It returnstrueif the stream is ready for I/O operations (like reading or writing) andfalseif 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::ifstreamobjects automatically close the file when they go out of scope (when the variablefileis destroyed).In the application, the constructor of the
BookmarkManagerclass calls theloadBookmarksFromFilemethod 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
bookmarksvector, resets the ID counter, and then attempts to load bookmarks from a file. If a line represents a valid bookmark, aBookmarkobject 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
loadBookmarksFromFilemethod. ---Using
std::istringstreamto Read Lines From a FileThe
std::istringstreamclass, also part of the C++ Standard Library, is designed for performing input operations on strings. This class enables treating astd::stringobject as a stream, allowing you to extract values as if you were reading fromstd::cinor other input streams.To use
std::istringstream, include the<sstream>header:#include <sstream>This allows you to declare an
std::istringstreamobject and then set its contents using the.str()method:std::istringstream linestream; linestream.str(line);Initially,
linestreamis declared as an empty stream. Then, the.str(line)method is used to populate the stream with the content of thelinestring.This way, data can be extracted using the extraction operator (
>>) or functions such asstd::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
linestreamup to theFIELD_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 invalue.In the
bookmarks.txtfile, 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::createBookmarkMethodEach 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::createBookmarkmethod is a static factory method used to create instances of derivedBookmarktypes (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
createFromFilemethod.Each
Bookmarksubclass has its owncreateFromFilestatic 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 forWebBookmark: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.hppfile, you can see the definition of thecreateBookmarkFromStreammethod: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, whereTis a subclass ofBookmark, 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::createFromFileonly needs to call the template methodcreateBookmarkFromStreambecause web bookmarks do not have additional fields beyond what theBookmarkbase class already handles. In contrast, forAcademicBookmarkandMediaBookmarkyou'll need to extract the additional fields they define. -
Challenge
Step 3: Saving Bookmarks to a File
Writing to Files with
std::ofstreamstd::ofstreamis a class provided by the C++ Standard Library that allows you to write data to files.std::ofstreamstands for output file stream, and to use it, you must include the<fstream>header in your program. This way, you can declare anstd::ofstreamobject 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
openmethod is called on theofstreamobject,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:-
std::ios::out: This flag is used to specify that the file stream is opened for writing, so it's the default mode forstd::ofstream. If you open a file withstd::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. -
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 withstd::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. -
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::outandios::trunc, usingstd::ios::truncalone does not imply writing or reading mode; it only specifies the behavior regarding the file's contents. However, when used withstd::ofstream, sincestd::ios::outis the default mode,std::ios::trunceffectively deletes all the content of a file before writing to it (as if opening the file withstd::ios::out | std::ios::trunc).In the next task, you'll modify the
BookmarkManager::saveBookmarksToFilemethod to open thebookmarks.txtfile for writing. ---Understanding
Bookmark::saveFieldsand Subclass ImplementationThe
Bookmark::saveFieldsmethod in theBookmarkbase class is designed to save the common fields of a bookmark (likeid,url,title, andtags) 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, andWebBookmark), allowing them to leverage the same functionality for saving common fields and extend it by overriding thesaveAdditionalFieldsmethod to save subclass-specific fields.The syntax
file << [VARIABLE] << separatoris 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. Theseparatoris used to delimit fields in the file, ensuring that when the bookmarks are loaded back, the fields can be correctly parsed.The
WebBookmarkclass does not override thesaveAdditionalFieldsmethod, since it does not have any fields beyond what is already defined in theBookmarkbase class.In contrast,
AcademicBookmarkandMediaBookmarkneed to implement this method to handle any fields that are specific to those bookmark types. You'll do this in the next tasks. -
-
Challenge
Step 4: Finding Bookmarks
Understanding
std::vectorstd::vectoris a sequence container in the C++ Standard Library that represents a dynamically resizable array.In the application, the bookmarks are stored in a
std::vectorwhere each element is aunique_ptrthat points to an object of a derived class ofBookmark(likeAcademicBookmark,MediaBookmark, orWebBookmark). You can see the definition of this vector in the fileinclude/BookmarkManager.hpp:class BookmarkManager { private: std::vector<std::unique_ptr<Bookmark>> bookmarks; // Rest of the class...std::unique_ptris a smart pointer that manages another object through a pointer and automatically disposes of that object when theunique_ptrgoes out of scope. This ensures automatic resource management, including memory deallocation, to prevent memory leaks.You can iterate over elements in a
std::vectorusing a range-basedforloop:std::vector<std::unique_ptr<Object>> container; for (const auto& element : container) { // Your code here }In this loop:
elementrefers to the current element in thecontainer.autoallows the compiler to deduce the type ofelement.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
forloop 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:
iis the loop index, starting from0.- The loop runs while
iis less than the container's size. static_cast<int>(container.size())converts the size (of typestd::size_t) tointto match the type ofi.
Inside the loop, you can access the element at the
i-th position in thecontainerusingcontainer[i]. To access the members of theBookmarkobject pointed to by aunique_ptr, use the->operator:container[i]->aField; container[i]->aMethod();The
findBookmarkIndexByIdmethod inBookmarkManageris used by other methods in the class to search for a bookmark with a matching ID and return its index in thebookmarksvector. You'll complete the implementation offindBookmarkIndexByIdin the following task. -
Challenge
Step 5: Adding and Removing Bookmarks
Adding Elements to a Vector
To append an element to a
std::vector, use thepush_backmethod, 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 aunique_ptrcannot be copied (to avoid multiple ownership issues), it must be moved usingstd::move. This function transfers the ownership of the object to anotherunique_ptr, leaving the originalunique_ptrin a null state.Consider the following example:
std::vector<std::unique_ptr<MyClass>> myVector; std::unique_ptr<MyClass> myPtr;To add
myPtrtomyVector, you would do:myVector.push_back(std::move(myPtr));After this,
myPtrwill no longer own the object (it will be null), andmyVectorwill take ownership of theunique_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
erasemethod is used to remove elements from astd::vector. It can delete either a single element or a range of elements.To remove a single element, provide
erasewith an iterator to the target element. The following example removes the element at a specificindex:vec.erase(vec.begin() + index);Here
vec.begin() + indexcalculates an iterator to the index-th element (considering 0-based indexing), which is then removed.For removing a range of elements,
eraserequires 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
startposition up to, but not including, theendposition.In both cases, the
beginmethod returns an iterator pointing to the first element of the vector. Since iterators support arithmetic operations, adding an index tobegin()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, sayindex = 3, use theerasemethod: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 (
4in this case) is removed, and the remaining elements are shifted left.Similarly, to remove a range from
start = 4toend = 7, theerasemethod 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, and7.In the next task, you will complete the implementation of a method to remove a bookmark from the vector containing all bookmarks.
-
Challenge
Step 6: Launching Bookmarks
Launching URLs and Using
std::asyncLaunching a URL in the default web browser is done by executing a system command, which varies depending on the operating system:
- Windows: Use the
startcommand followed by the URL. Example:start http://example.com. - macOS: Use the
opencommand followed by the URL. Example:open http://example.com. - Linux: Use the
xdg-opencommand 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
0typically 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"); #endifThese 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::threadand 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::futureObject: If you need to ensure that the tasks complete and you might want to check their status or results later, you can utilize astd::futureobject by usingstd::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::asyncis preferred when the command's return value matters.std::asyncruns a function asynchronously (potentially in a new thread) and returns astd::futureobject 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 thestd::future::getmethod.
So, to use
std::async, include the<future>header and callstd::asyncwithstd::launch::asyncto 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::systemcall, allowing the program to continue execution without waiting for the command to complete.In the
BookmarkManager::launchBookmarkmethod, you'll usestd::asyncwith thestd::launch::asyncpolicy to launch a bookmark URL asynchronously. ---Managing
std::futureObjectsThe destructor of the
std::futureobject returned bystd::asyncblocks until the associated task completes if launched asynchronously (usingstd::launch::async). This ensures that a task is properly completed before thestd::futureis destroyed.If the
std::futureis 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 thestd::futureobjects 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::futureobject for later, so storing thestd::futureobject isn't necessary.You can use the
std::futureobject to retrieve the return value of thestd::systemcall. Remember:- A return value of
0usually 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::getmethod. 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 usingstd::future::wait_forwith 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 thestd::futureis associated with a shared state.wait_for()waits for a specified duration for the operation to complete. It returns an enum of typestd::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 eachstd::future, consuming the shared state.
This approach uses the
std::futureobject immediately to wait for task completion and retrieve results. After checking the result, thestd::futureobject goes out of scope and is destroyed automatically.In the next task, you'll use
fut.wait_forwith 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 withfuture.get(). If the task isn't ready (thewait_forcall times out), it logs a timeout message and returnsfalse.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.
- Windows: Use the
-
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:
-
Compilation: Clicking Run will compile all the files in the
srcdirectory into an executable namedBookmarkManager. -
Running the Program: After compilation, the program will automatically execute using the command:
./BookmarkManagerThere is a
bookmarks.txtfile 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 thelaunch.logfile 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:
-
Separate file I/O operations: Create a new class, for example,
BookmarkFileHandler, to handle all file-related operations. This will not only make theBookmarkManagercleaner but also adheres to the Single Responsibility Principle by delegating file operations to a dedicated class. -
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.
-
Create a command interface or class: Following the Command design pattern, consider defining a
Commandabstract base class with anexecute()method. Each command (add,list,remove,launch, andhelp) could be a derived class. This approach means adding new commands would not require extensive modifications to theCLIclass. -
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.
-
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-catchblocks 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++.
-
About the author
Real skill practice before real-world application
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.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.