- Lab
- 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.

Path 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|author
Upon launch, the application reads this file and stores the bookmarks in a vector as instances of three classes:
WebBookmark
,MediaBookmark
, andAcademicBookmark
, all subclasses of theBookmark
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:
-
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 theWebBookmark
class, which is a subclass of theBookmark
class 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 theAcademicBookmark
class, which is a specialized subclass of theBookmark
class for academic-related bookmarks. This class is declared with additional member variables specific to academic bookmarks:publicationDate
andauthors
. -
src/bookmark/MediaBookmark.cpp
: Implements theMediaBookmark
class, which is a subclass of theBookmark
class tailored for media-related bookmarks. This class is declared with additional member variables specific to media bookmarks:mediaType
andduration
. -
src/BookmarkManager.cpp
: Implements theBookmarkManager
class, 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 theCLI
class, which provides the user interface for interacting with theBookmarkManager
class in the application. It handles all interactions with the user, delegating bookmark management tasks to theBookmarkManager
class. -
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 theUtils
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 theAcademicBookmark
andMediaBookmark
subclasses. As you progress, you'll understand how the other files interplay with these classes. Each file, along with its header file in theinclude
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.
-
-
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 asid
,url
,title
, andtags
. It also includes common methods likeprintDetails()
andsaveFields()
. Subclasses, such asAcademicBookmark
andMediaBookmark
, extend theBookmark
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 theBookmark
class 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 theBookmark
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
andMediaBookmark
overridegetTypeIdentifier()
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
WebBookmark
does not overrideprintAdditionalDetails()
, indicating that web bookmarks do not have additional details beyond those stored in theBookmark
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()
andprintAdditionalDetails()
methods within theAcademicBookmark
andMediaBookmark
subclasses. -
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 namefile
of typestd::ifstream
.file.open("example.txt");
attempts to openexample.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 fromfile
until a newline character is found, storing the text in theline
string. 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::ofstream
andstd::fstream
) can be implicitly converted to aboolean
. It returnstrue
if the stream is ready for I/O operations (like reading or writing) andfalse
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 variablefile
is destroyed).In the application, the constructor of the
BookmarkManager
class calls theloadBookmarksFromFile
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, aBookmark
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 FileThe
std::istringstream
class, also part of the C++ Standard Library, is designed for performing input operations on strings. This class enables treating astd::string
object as a stream, allowing you to extract values as if you were reading fromstd::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 theline
string.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
linestream
up 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.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
MethodEach 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 derivedBookmark
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 owncreateFromFile
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 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.hpp
file, you can see the definition of thecreateBookmarkFromStream
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
, whereT
is 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::createFromFile
only needs to call the template methodcreateBookmarkFromStream
because web bookmarks do not have additional fields beyond what theBookmark
base class already handles. In contrast, forAcademicBookmark
andMediaBookmark
you'll need to extract the additional fields they define. -
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 anstd::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 theofstream
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:-
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::out
andios::trunc
, usingstd::ios::trunc
alone does not imply writing or reading mode; it only specifies the behavior regarding the file's contents. However, when used withstd::ofstream
, sincestd::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 withstd::ios::out | std::ios::trunc
).In the next task, you'll modify the
BookmarkManager::saveBookmarksToFile
method to open thebookmarks.txt
file for writing. ---Understanding
Bookmark::saveFields
and Subclass ImplementationThe
Bookmark::saveFields
method in theBookmark
base 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 thesaveAdditionalFields
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. Theseparator
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 thesaveAdditionalFields
method, since it does not have any fields beyond what is already defined in theBookmark
base class.In contrast,
AcademicBookmark
andMediaBookmark
need 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::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 aunique_ptr
that 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_ptr
is a smart pointer that manages another object through a pointer and automatically disposes of that object when theunique_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-basedfor
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 thecontainer
.auto
allows 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
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 from0
.- The loop runs while
i
is less than the container's size. static_cast<int>(container.size())
converts the size (of typestd::size_t
) toint
to match the type ofi
.
Inside the loop, you can access the element at the
i
-th position in thecontainer
usingcontainer[i]
. To access the members of theBookmark
object pointed to by aunique_ptr
, use the->
operator:container[i]->aField; container[i]->aMethod();
The
findBookmarkIndexById
method inBookmarkManager
is used by other methods in the class to search for a bookmark with a matching ID and return its index in thebookmarks
vector. You'll complete the implementation offindBookmarkIndexById
in 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_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 aunique_ptr
cannot 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_ptr
in a null state.Consider the following example:
std::vector<std::unique_ptr<MyClass>> myVector; std::unique_ptr<MyClass> myPtr;
To add
myPtr
tomyVector
, you would do:myVector.push_back(std::move(myPtr));
After this,
myPtr
will no longer own the object (it will be null), andmyVector
will 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
erase
method 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
erase
with an iterator to the target element. The following example removes the element at a specificindex
: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, theend
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 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 theerase
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
toend = 7
, theerase
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
, 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::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 astd::future
object 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::async
is preferred when the command's return value matters.std::async
runs a function asynchronously (potentially in a new thread) and returns astd::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 thestd::future::get
method.
So, to use
std::async
, include the<future>
header and callstd::async
withstd::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 usestd::async
with thestd::launch::async
policy to launch a bookmark URL asynchronously. ---Managing
std::future
ObjectsThe destructor of the
std::future
object returned bystd::async
blocks until the associated task completes if launched asynchronously (usingstd::launch::async
). This ensures that a task is properly completed before thestd::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 thestd::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 thestd::future
object isn't necessary.You can use the
std::future
object to retrieve the return value of thestd::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 usingstd::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 thestd::future
is 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::future
object immediately to wait for task completion and retrieve results. After checking the result, thestd::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 withfuture.get()
. If the task isn't ready (thewait_for
call 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
src
directory into an executable namedBookmarkManager
. -
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 thelaunch.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:
-
Separate file I/O operations: Create a new class, for example,
BookmarkFileHandler
, to handle all file-related operations. This will not only make theBookmarkManager
cleaner 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
Command
abstract 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 theCLI
class. -
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-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++.
-
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.