I wrote a menu user interface in C++. What do you think about my code? What can I improve? Am I missing some better practices?
main.cpp
// the purpose of the below code is to test menu class
#include <ios>
#include <iostream>
#include <limits>
#include <string>
#include <utility>
#include "menu.h"
/*
* these functions are defined only to test menu.h library
* i plan to merge these functions to the CalculatorApp class
* in the future
*
* i wrote below code in a hurry
*/
// helper functions; will be moved into utils.h/utils.cpp in the future
inline void clear_console_input() {
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
double read_double(const std::string& prompt = "Enter the number: ") {
double number;
bool success{false};
do {
std::cout << prompt;
if (std::cin >> number) {
success = true;
} else {
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::cout << "Error: not a number." << std::endl;
}
} while (!success);
return number;
}
std::pair<double, double> read_operands() {
double first{read_double("Enter the first operand: ")};
double second{read_double("Enter the second operand: ")};
clear_console_input();
return std::make_pair(first, second);
}
std::pair<double, double> read_operands_div() {
double first{read_double("Enter the first operand: ")};
double second;
while ((second = read_double("Enter the second operand: ")) == 0) {
std::cout << "Error: number is equal to zero." << std::endl;
}
clear_console_input();
return std::make_pair(first, second);
}
// calculator interface
void add() {
auto numbers = read_operands();
std::cout << "Result of " << numbers.first << " + " << numbers.second
<< " is " << numbers.first + numbers.second << std::endl;
}
void subtract() {
auto numbers = read_operands();
std::cout << "Result of " << numbers.first << " - " << numbers.second
<< " is " << numbers.first - numbers.second << std::endl;
}
void multiply() {
auto numbers = read_operands();
std::cout << "Result of " << numbers.first << " * " << numbers.second
<< " is " << numbers.first * numbers.second << std::endl;
}
void divide() {
auto numbers = read_operands_div();
std::cout << "Result of " << numbers.first << " / " << numbers.second
<< " is " << numbers.first / numbers.second << std::endl;
}
int main() {
menu app{"Calculator"};
app.add_item("Add", add);
app.add_item("Subtract", subtract);
app.add_item("Multiply", multiply);
app.add_item("Divide", divide);
menu menu2{"test"};
app.add_submenu(menu2);
app.loop();
std::cout << "Good bye!\n";
return 0;
}
menu.h
#ifndef MENU_H
#define MENU_H
#include <functional>
#include <iostream>
#include <string>
#include <vector>
struct menu_item {
explicit menu_item(const std::string& title_val) : title{title_val}, action{noop} {}
menu_item(const std::string& title_val, std::function<void()> action_val)
: title{title_val}, action{action_val} {}
menu_item(const menu_item& item) : title{item.title}, action{item.action} {}
friend std::ostream& operator<<(std::ostream& lhs, const menu_item& rhs);
static void noop() { std::cout << "No action defined!\n"; }
std::string title{};
std::function<void()> action{};
};
class menu {
public:
explicit menu(const std::string& title_val);
menu(const std::string& title_val, const std::vector<menu_item>& items_val);
menu(const std::string& title_val, const std::string& description_val);
menu(const std::string& title_val, const std::string& description_val,
const std::vector<menu_item>& items_val);
void add_item(const menu_item& item);
void add_item(const std::string& title_val, std::function<void()> action);
void add_item_at(int index, const menu_item& item);
void add_item_at(int index, const std::string& title_val,
std::function<void()> action);
void remove_item(int index);
void add_submenu(const menu& menu_val);
void add_submenu(const std::string& title_val, const menu& menu_val);
void add_submenu_at(int index, const menu& menu_val);
void add_submenu_at(int index, const std::string& title_val,
const menu& menu_val);
void set_title(const std::string& title_val);
void set_description(const std::string& description_val);
void loop() const;
private:
// helper functions
void print_choices() const;
char read_choice() const;
void check_index(std::size_t index) const;
// wrapper function for menu::loop
std::function<void()> invoke_menu_loop(const menu& menu_val);
static int nesting_level;
std::string title{};
std::string description{};
std::vector<menu_item> items{};
};
#endif // MENU_H
menu.cpp
#include "menu.h"
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
static constexpr char quit_choice = 'q';
// helper functions
static void print_title(const std::string& title) {
std::size_t title_len = title.size();
std::size_t width = title_len + 4;
for (std::size_t i = 0; i < width; ++i) {
std::cout << "*";
}
std::cout << "\n* " << title << " *\n";
for (std::size_t i = 0; i < width; ++i) {
std::cout << "*";
}
std::cout << std::endl;
}
// menu_item struct i/o operators
std::ostream& operator<<(std::ostream& lhs, const menu_item& rhs) {
return lhs << rhs.title;
}
// menu class constructors
menu::menu(const std::string& title_val) : title{title_val} {}
menu::menu(const std::string& title_val,
const std::vector<menu_item>& items_val)
: title{title_val}, items{items_val} {}
menu::menu(const std::string& title_val, const std::string& description_val)
: title{title_val}, description{description_val} {}
menu::menu(const std::string& title_val, const std::string& description_val,
const std::vector<menu_item>& items_val)
: title{title_val}, description{description_val}, items{items_val} {}
// menu class static variables
int menu::nesting_level{0};
// menu class public interface
void menu::add_item(const menu_item& item) { items.push_back(item); }
void menu::add_item(const std::string& title_val, std::function<void()> action_val) {
items.push_back(menu_item{title_val, action_val});
}
void menu::add_item_at(int index, const menu_item& item) {
check_index(index);
items.insert(items.begin() + index, item);
}
void menu::add_item_at(int index, const std::string& title_val,
std::function<void()> action) {
check_index(index);
items.insert(items.begin() + index, menu_item{title_val, action});
}
void menu::remove_item(int index) {
check_index(index);
items.erase(items.begin() + index);
}
void menu::add_submenu(const menu& menu_val) {
items.push_back(menu_item{menu_val.title, invoke_menu_loop(menu_val)});
}
void menu::add_submenu(const std::string& title_val, const menu& menu_val) {
items.push_back(menu_item{title_val, invoke_menu_loop(menu_val)});
}
void menu::add_submenu_at(int index, const menu& menu_val) {
check_index(index);
items.insert(items.begin() + index,
menu_item{menu_val.title, invoke_menu_loop(menu_val)});
}
void menu::add_submenu_at(int index, const std::string& title_val,
const menu& menu_val) {
check_index(index);
items.insert(items.begin() + index,
menu_item{title_val, invoke_menu_loop(menu_val)});
}
void menu::set_title(const std::string& title_val) { title = title_val; }
void menu::set_description(const std::string& description_val) {
description = description_val;
}
void menu::loop() const {
bool active = true;
++nesting_level;
while (active) {
print_title(title);
if (!description.empty()) {
std::cout << std::endl;
std::cout << description;
std::cout << std::endl;
}
char choice = read_choice();
if (choice == quit_choice) {
--nesting_level;
active = false;
} else {
std::cout << '\n';
// call function bound to menu item
items[choice - '0'].action();
// wait until user press the enter key
std::cout << "\nPress enter to continue...\n";
std::cin.get();
}
}
}
// menu class private functions
void menu::print_choices() const {
for (std::size_t i = 0; i < items.size(); ++i) {
std::cout << i << ") " << items[i] << std::endl;
}
std::cout << "q) ";
if (nesting_level > 1)
std::cout << "Back";
else
std::cout << "Quit";
std::cout << std::endl;
}
char menu::read_choice() const {
std::string user_input;
char choice;
bool valid{false};
std::cout << "What do you want to do?\n";
do {
print_choices();
std::cout << "Your choice: ";
std::getline(std::cin, user_input);
if (user_input.length() > 1 || user_input.empty()) {
std::cout << "Please enter the valid choice.\n";
continue;
}
choice = tolower(user_input.at(0));
if ((choice - '0') < 0 ||
(static_cast<std::size_t>(choice - '0') >= items.size() &&
(choice != quit_choice))) {
std::cout << "Invalid choice!\n";
} else {
valid = true;
}
} while (!valid);
return choice;
}
void menu::check_index(std::size_t index) const {
const std::size_t max_allowed_idx = items.size() - 2;
if (index > max_allowed_idx) {
std::ostringstream oss;
oss << "index value must be between " << index << " and "
<< max_allowed_idx;
throw std::out_of_range{oss.str()};
}
}
// wrapper function for menu::loop
std::function<void()> menu::invoke_menu_loop(const menu& the_menu) {
return [&the_menu]() { the_menu.loop(); };
}