Сравнение улучшения кода в Python и C++ (часть 2)
Данная статья посвящена продолжению темы улучшения кода в языках Python и C++, а именно обработке ошибок, валидации, аннотациям типов, повышению отказоустойчивости и пере использованию кода.
Дополнительные материалы
Для скачивания материалов необходимо войти или зарегистрироваться
Файлы также можно получить в Telegram-боте по коду: 155910
Реклама
Вступление
Всем доброго дня! В предыдущей статье Сравнение улучшения кода в Python и C++ (часть 1) из рубрики "Сравнение Python и C/C++" мы улучшили наш код Python версии телефонной книги. Мы повысили отказоустойчивость, улучшили читаемость кода, применили аннотации типов, обработали ошибки и т.д. Сегодня мы постараемся сделать нечто подобное с C++ версией телефонной книги. Открывайте свои IDE и приготовьтесь делать нашу телефонную книгу на C++ лучше.
Явная типизация вместо аннотации
В отличие от языка Python c его динамической типизацией в C++ типизация статическая. Это значит что типы данных определяются во время компиляции.
Давайте рассмотрим функцию getTitle()
и её типы данных:
// Функция для получения названия контакта (фи)
std::string getTitle(const std::string& firstName, const std::string& lastName){
std::ostringstream oss;
oss << firstName << " " << lastName;
std::string newTitle = oss.str();
return newTitle;
}
Мы видим что параметры функции const std::string& firstName, const std::string& lastName
имеют тип std::string
. В C++ обычно принято указывать тип переменной явно. Так как типизация статическая переменная уже не сможет поменять тип данных. Правда с версии C++11 есть один способ сделать определение типа в зависимости от содержимого переменной написав auto перед переменной.
Давайте посмотрим на подобный пример:
auto x = 10;
x = "test";
В примере auto x = 10;
где переменная x
будет иметь тип данных int
. Однако такой подход не меняет тот факт что типизация статическая и если попытаться присвоить строку в переменной x = "test";
вы получите ошибку при компиляции.
Начиная с C++17 появляется std::variant
, который позволяет хранить значение одного из нескольких заранее определённых типов. Это расширяет возможности работы с типами, оставаясь в рамках статической типизации. Ссылку на него я оставлю в описании к интересной статье на Habr.
Явная типизация на практике
При статической типизации в C++ программист обычно явно указывает тип данных, в отличие от Python, где аннотация типов необязательна. Ещё перед функцией в C++ мы пишем std::string getTitle
что указывает на то что функция должна вернуть тип данных std::string
. Если функция вернёт другой тип данных, то у вас будет ошибка на этапе запуска.
Давайте попробуем заменить строку return newTitle; на return 0;, а затем скомпилировать и запустить код:
$ g++ -o phone_book_cpp phone_book.cpp
Код успешно скомпилировался, так как на этапе компиляции компилятор не обнаружил ошибок в синтаксисе и проверке типов. Однако ошибка возникнет при выполнении программы.
$ ./phone_book_cpp
terminate called after throwing an instance of 'std::logic_error'
what(): basic_string: construction from null is not valid
Аварийный останов (образ памяти сброшен на диск)
Почему возникла данная ошибка? Проблема заключается в том, что в строке вызова функции std::string title = getTitle("Николай", "Николаев");
мы переменной с std::string
типом пытаемся присвоить int
тип, который вернула нам функция getTitle()
.
Давайте посмотрим, что произойдёт, если мы зададим переменной тип int
и вызовем функцию следующим образом:
int newTitle = getTitle("Сергей", "Сергеев");
$ g++ -o phone_book_cpp phone_book.cpp
phone_book.cpp: In function ‘int main()’:
phone_book.cpp:60:28: error: cannot convert ‘std::string’ {aka ‘std::__cxx11::basic_string<char>’} to ‘int’ in initialization
60 | int newTitle = getTitle("Сергей", "Сергеев");
| ~~~~~~~~^~~~~~~~~~~~~~~~~~~~~
| |
| std::string {aka std::__cxx11::basic_string<char>}
Ошибка произошла на этапе компиляции. Причина в том, что функция getTitle()
объявлена с возвращаемым типом std::string
, но в строке return 0;
возвращается значение типа int
. Компилятор C++ сравнивает объявленный возвращаемый тип функции и реальный тип возвращаемого значения и фиксирует их несоответствие.
В отличие от C++, Python использует динамическую типизацию, но она также строгая. Если попытаться сложить число и строку, как в примере.
print(9 + '9')
Вы получите ошибку TypeError
, поскольку Python строго контролирует типы во время выполнения, не допуская таких операций.
По итогу в C++ мы явно указываем тип данных, что показывает какой тип данных переменная принимает. Так же возвращаемое значение в C++ мы пишем вначале функции. Тип переменной устанавливается во время компиляции и поменять его не получиться.
Улучшение C++ кода
Наш код на C++, написанный в статье Сравнение hashmap C/C++ с dict Python (часть 3) можно улучшить. Ещё в ней можно ознакомиться с основами C++, необходимыми для понимания текущей статьи, посвящённой улучшению кода. Начнём с переменной phoneBook
, в которой хранится hash map телефонной книги.
Переменная phoneBook
// Переменная для хранения map телефонной книги
std::unordered_map<std::string, std::string> phoneBook = {
{"Николай Николаев", "+72423535354"},
{"Виталий Краснов", "84424434345"}
};
Код работает корректно, но размещение переменной phoneBook
в глобальной области видимости может привести к проблемам. От этой переменной зависят другие функции, поэтому её текущее местоположение требует особого внимания при изменении или тестировании кода. Это усложняет поддержку и делает функции менее независимыми.
Для устранения этих проблем переместим переменную в основную функцию main()
. В этой функции уже находятся вызовы всех остальных функций, а также данные, необходимые для работы с контактами. Это упростит редактирование телефонной книги, улучшит читаемость кода и сделает наши функции независимыми от переменной phoneBook
, позволяя осуществлять их повторное использование и тестирование.
Функция getTitle
Перейдём к функции getTitle()
, которая получает строку фи через пробел из двух отдельных строк имени и фамилии.
// Функция для получения названия контакта (фи)
std::string getTitle(const std::string& firstName, const std::string& lastName){
std::ostringstream oss;
oss << firstName << " " << lastName;
std::string newTitle = oss.str();
return newTitle;
}
В текущем варианте функция использует поток для создания новой строки, что избыточно и делает код громоздким. Вместо этого можно оптимизировать её, заменив четыре строки тела функции одной строкой.
return firstName + " " + lastName;
Также, начиная с C++20
, доступен более современный способ с использованием std::format
. Для этого потребуется подключить заголовочный файл #include <format>
и использовать следующую конструкцию.
return std::format("{} {}", firstName, lastName);
Эта запись выглядит лаконично и экономит память. Она напоминает метод .format()
в Python.
Однако наиболее эффективным решением будет полностью удалить функцию из кода. Данный функционал избыточен и не добавляет ценности нашей телефонной книге, поскольку её можно заменить встроенными средствами языка. Поэтому мы убираем функцию getTitle()
.
Функция addContact
Далее откроем функцию addContact()
, которая добавляет новый контакт в телефонную книгу.
// Функция для добавления контакта
void addContact(const std::string& title, const std::string& phoneNumber) {
phoneBook[title] = phoneNumber;
}
На первый взгляд функция выглядит лаконично, но в текущем виде она имеет несколько недостатков. Во-первых, она зависит от глобальной переменной phoneBook
, что снижает её универсальность и тестируемость. Во-вторых, нам нужно отказаться от параметра title
и использовать вместо него два параметра - firstName
и lastName
, чтобы убрать избыточную функцию getTitle()
. Также было бы полезно добавлять сообщение в терминал об успешном добавлении контакта.
Давайте перепишем эту функцию:
// Функция для добавления контакта
void addContact(const std::string& firstName,
const std::string& lastName,
const std::string& phoneNumber,
std::unordered_map<std::string, std::string>& phoneBook) {
const std::string contact = firstName + " " + lastName;
phoneBook[contact] = phoneNumber;
std::cout << "Контакт " << contact
<< " с номером " << phoneNumber
<< " успешно добавлен." << std::endl;
}
Теперь наша функция принимает четыре аргумента: firstName
(имя), lastName
(фамилия), phoneNumber
(номер телефона) и phoneBook
(телефонная книга). Для удобства чтения параметры функции переносятся на отдельные строки. В гайде по стилю от Google рекомендуется не превышать длину строки в 80 символов. Хотя некоторые проекты допускают до 100 символов, я лично придерживаюсь ограничения в 80 символов - это улучшает читаемость кода на небольших экранах, например, на смартфоне. Это правило я применяю не только в C++, но и в Python..
В строке const std::string contact = firstName + " " + lastName;
я использую конкатенацию для создания строки контакта. Строка std::cout << "Контакт " + contact + " с номером " + phoneNumber + " успешно добавлен." << std::endl;
выводит нам сообщение о том что контакт успешно добавлен. Такой подход делает функцию более универсальной, понятной и удобной в использовании.
Функция getNumber
Перейдём к функции getNumber()
, которая возвращает номер телефона или сообщение "Контакт не найден!"
, если контакт не был найден в телефонной книге.
// Функция для получения номера по имени
std::string getNumber(const std::string& title) {
auto contact = phoneBook.find(title);
if (contact != phoneBook.end()) {
return contact->second;
}
return "Контакт не найден!";
}
Не буду снова заострять внимание на параметрах title
, phoneBook
, firstName
и lastName
- это уже очевидные вещи. Однако в этой функции есть несколько моментов, на которых я хотел бы остановиться.
Во-первых, мы будем формировать строку с фи прямо в функции с помощью конкатенации.
Во-вторых, вместо возврата строки "Контакт не найден!"
как результата мы можем вывести это сообщение в терминал с помощью std::cout
.
Также неплохо бы добавить возможность вернуть значение, аналогичное None
в Python, если контакт не найден.
Давайте перепишем эту функцию и посмотрим, что из этого получится:
// Функция для получения номера по имени
std::optional<std::string> getNumber(const std::string& firstName,
const std::string& lastName,
const std::unordered_map<std::string,
std::string>& phoneBook) {
const std::string contact = firstName + " " + lastName;
auto searchContact = phoneBook.find(contact);
if (searchContact != phoneBook.end()) {
return searchContact->second;
}
std::cout << "Контакт не найден!" << std::endl;
return std::nullopt;
}
Как видно в обновленной функции, её тип изменился на std::optional
. Мы использовали std::optional
, чтобы функция могла либо вернуть строку, либо вернуть std::nullopt
, что является аналогом None
в Python. В Python функция автоматически вернёт None
, если ничего не вернёт, но в C++ для этого используется std::nullopt
.
Строку фи мы получаем конкатенацией const std::string contact = firstName + " " + lastName;
. Строка std::cout << "Контакт не найден!" << std::endl;
выведет нам в terminal если контакт не найден в телефонной книге. Если контакт не найден функция вернёт нам return std::nullopt;
, который можно использовать для логики программы, чтобы корректно обработать отсутствие контакта.
Функция getAllContacts
Следующая функция getAllContacts()
выводит все контакты в terminal.
// Функция для вывода всех контактов
void getAllContacts() {
size_t count = phoneBook.size();
if (count <= 0) {
std::cout << "Hash map пуста!" << std::endl;
}
else {
for (const auto& contact : phoneBook) {
std::cout << contact.first << " - " << contact.second << std::endl;
}
}
}
Функция в целом написана правильно, но если телефонная книга пуста, вместо сообщения "Hash map пуста!"
, я бы предложил вывести "Телефонная книга пуста!"
. Это будет более понятно для пользователя телефонной книги, который может быть не знаком с термином hash map. Ещё я заметил небольшую ошибку в условии if (count <= 0) {
. Количество контактов не может быть отрицательным в телефонной книге.
Также, как и в других функциях, необходимо передавать phoneBook
в параметр функции. Вот как выглядит исправленная версия.
// Функция для вывода всех контактов
void getAllContacts(const std::unordered_map<std::string,
std::string>& phoneBook) {
size_t count = phoneBook.size();
if (count 0) {
std::cout << "Телефонная книга пуста!" << std::endl;
}
else {
for (const auto& contact : phoneBook) {
std::cout << contact.first << " - " << contact.second << std::endl;
}
}
}
Функция delContact
Следующей идёт маленькая, но в то же время важная функция delContact()
, необходимая для удаления контакта.
// Функция для удаления контакта
void delContact(const std::string& title) {
phoneBook.erase(title);
}
Здесь также нужно будет принять параметры phoneBook
, firstName
и lastName
, убрав title
. В случае успеха необходимо вывести в терминал сообщение о том, что контакт был удален. Если контакт не найден, выведем сообщение о том, что удаление не было выполнено.
Давайте приступим к улучшению функции:
// Функция для удаления контакта
void delContact(const std::string& firstName,
const std::string& lastName,
std::unordered_map<std::string, std::string>& phoneBook) {
const std::string contact = firstName + " " + lastName;
auto searchContact = phoneBook.find(contact);
if (searchContact != phoneBook.end()) {
phoneBook.erase(contact);
std::cout << "Контакт " << contact << " успешно удалён." << std::endl;
}
else {
std::cout << "Контакт не найден!" << std::endl;
}
}
Наша новая измененная функция имеет общие черты с функцией getNumber()
. В строке auto searchContact = phoneBook.find(contact);
мы ищем контакт в телефонной книге. В условии if (searchContact != phoneBook.end()) {
мы проверяем, не достигли ли мы конца unordered_map
. Если мы его не достигаем, это значит, что мы нашли контакт и можем удалить его в строке phoneBook.erase(contact);
. Далее выводим информацию об успешном удалении контакта в строке std::cout << "Контакт " << contact << " успешно удален." << std::endl;
. В блок else
попадаем, если не найдем контакт в телефонной книге, и выведем сообщение о том, что его не нашли, в строке std::cout << "Контакт не найден!" << std::endl;
.
Функция delAllContacts
Следующая функция delAllContacts()
удалит все контакты из телефонной книги.
// Функция для удаления всех контактов
void delAllContacts() {
phoneBook.clear();
}
Эта функция имеет те же недостатки, что и предыдущая функция, которая удаляет один контакт. Единственным отличием является то, что вместо поиска контакта мы проверяем, пуста ли телефонная книга. Нет смысла запускать операцию очистки для пустой телефонной книги.
Давайте перейдём к улучшению функции:
// Функция для удаления всех контактов
void delAllContacts(std::unordered_map<std::string, std::string>& phoneBook) {
if (!phoneBook.empty()) {
phoneBook.clear();
std::cout << "Теелефонная книга успешно очищена от контактов."
<< std::endl;
}
else {
std::cout << "Телефонная книга пуста и очистка не требуется!"
<< std::endl;
}
}
Теперь наша обновлённая функция проверяет, пуста ли телефонная книга с помощью if (!phoneBook.empty()) {
. Если телефонная книга не пуста, функция очищает её с помощью phoneBook.clear();
и выводит сообщение об успешном очищении std::cout << "Телефонная книга успешно очищена от контактов." << std::endl;
. Если телефонная книга пуста, выводится сообщение о том, что очистка не требуется: std::cout << "Телефонная книга пуста, очистка не требуется!" << std::endl;
.
Я применил if (!phoneBook.empty())
вместо if (count 0)
, потому что использование этого метода гораздо удобнее, чем получение размера hash map
и его последующая проверка на равенство 0.
В функции getAllContacts()
я перепишу код, таким образом, чтобы было возможно использовать метод .empty()
, поскольку необходимо соблюдать единообразие стилистики написания кода.
// Функция для вывода всех контактов
void getAllContacts(const std::unordered_map<std::string,
std::string>& phoneBook) {
if (!phoneBook.empty()) {
for (const auto& contact : phoneBook) {
std::cout << contact.first << " - " << contact.second << std::endl;
}
}
else {
std::cout << "Телефонная книга пуста!" << std::endl;
}
}
Теперь давайте посмотрим что получилось в итоге.
Итоговый результат
#include <iostream>
#include <unordered_map>
#include <string>
#include <sstream>
#include <optional>
// Функция для добавления контакта
void addContact(const std::string& firstName,
const std::string& lastName,
const std::string& phoneNumber,
std::unordered_map<std::string, std::string>& phoneBook) {
const std::string contact = firstName + " " + lastName;
phoneBook[contact] = phoneNumber;
std::cout << "Контакт " << contact
<< " с номером " << phoneNumber
<< " успешно добавлен." << std::endl;
}
// Функция для получения номера по имени
std::optional<std::string> getNumber(const std::string& firstName,
const std::string& lastName,
const std::unordered_map<std::string,
std::string>& phoneBook) {
const std::string contact = firstName + " " + lastName;
auto searchContact = phoneBook.find(contact);
if (searchContact != phoneBook.end()) {
return searchContact->second;
}
std::cout << "Контакт не найден!" << std::endl;
return std::nullopt;
}
// Функция для вывода всех контактов
void getAllContacts(const std::unordered_map<std::string,
std::string>& phoneBook) {
if (!phoneBook.empty()) {
for (const auto& contact : phoneBook) {
std::cout << contact.first << " - " << contact.second << std::endl;
}
}
else {
std::cout << "Телефонная книга пуста!" << std::endl;
}
}
// Функция для удаления контакта
void delContact(const std::string& firstName,
const std::string& lastName,
std::unordered_map<std::string, std::string>& phoneBook) {
const std::string contact = firstName + " " + lastName;
auto searchContact = phoneBook.find(contact);
if (searchContact != phoneBook.end()) {
phoneBook.erase(contact);
std::cout << "Контакт " << contact << " успешно удалён." << std::endl;
}
else {
std::cout << "Контакт не найден!" << std::endl;
}
}
// Функция для удаления всех контактов
void delAllContacts(std::unordered_map<std::string, std::string>& phoneBook) {
if (!phoneBook.empty()) {
phoneBook.clear();
std::cout << "Теелефонная книга успешно очищена от контактов."
<< std::endl;
}
else {
std::cout << "Телефонная книга пуста и очистка не требуется!"
<< std::endl;
}
}
// Главная функция
int main() {
// Переменная для хранения map телефонной книги
std::unordered_map<std::string, std::string> phoneBook = {
{"Николай Николаев", "+72423535354"},
{"Виталий Краснов", "84424434345"}
};
// Добавляем контакт
addContact("Сергей", "Сергеев", "83201345024", phoneBook);
std::cout << "--------------------------------" << std::endl;
// Получаем количество контактов
size_t count = phoneBook.size();
std::cout << "Количество контактов: " << count << std::endl;
std::cout << "--------------------------------" << std::endl;
// Получаем номер по имени и фамилии
std::optional<std::string> numberOpt = getNumber(
"Николай",
"Николаев",
phoneBook
);
if (numberOpt) {
std::string number = *numberOpt;
std::cout << number << std::endl;
}
std::cout << "--------------------------------" << std::endl;
// Выводим все контакты
getAllContacts(phoneBook);
std::cout << "--------------------------------" << std::endl;
// Удаляем контакт
delContact("Николай", "Николаев", phoneBook);
std::cout << "--------------------------------" << std::endl;
// Выводим все контакты (для проверки после удаления)
getAllContacts(phoneBook);
std::cout << "--------------------------------" << std::endl;
// Удаляем все контакты
delAllContacts(phoneBook);
std::cout << "--------------------------------" << std::endl;
// Выводим все контакты (когда hash map пуста)
getAllContacts(phoneBook);
return 0;
}
Компилируем наш код:
$ g++ -o phone_book_cpp phone_book.cpp
Запускаем код из terminal находясь в папке Сode improvement
:
./phone_book_cpp
Итоговый результат работы кода в terminal:
Контакт Сергей Сергеев с номером 83201345024 успешно добавлен.
--------------------------------
Количество контактов: 3
--------------------------------
+72423535354
--------------------------------
Сергей Сергеев - 83201345024
Виталий Краснов - 84424434345
Николай Николаев - +72423535354
--------------------------------
Контакт Николай Николаев успешно удалён.
--------------------------------
Сергей Сергеев - 83201345024
Виталий Краснов - 84424434345
--------------------------------
Теелефонная книга успешно очищена от контактов.
--------------------------------
Телефонная книга пуста!
Наш код стал лучше и работает так, как нужно. Теперь мы видим все важные сообщения, необходимые пользователю для понимания работы телефонной книги. Код стал более удобным для переноса, а его логика стала более понятной. Однако, мы еще не закончили улучшать наш код, поэтому в следующей статье нас ждет продолжение работы над улучшением телефонной книги на C++.
Анонс на следующие статьи
Следующая часть статьи выйдет 16.01.25, где я продолжу улучшать код телефонной книги в версии С++. Нам предстоит разобрать три важные темы в языке C++, которые не влезли во вторую часть статьи. Мы научимся обрабатывать ошибки, писать декораторы, а также попробуем написать аналог генераторной функции.
Дальше код будет ещё интереснее :)
Заключение
- Поговорили о статической типизации C++ как аналоге аннотаций Python;
- Узнали, как можно улучшить код;
- Сделали код более удобным и переносимым;
Поздравление с наступающим
Дорогие читатели, я хочу поздравить вас с наступающим Новым 2025 годом. Пожелать вам интересной IT работы, здоровья и творческих успехов. Наступающий год будет годом змеи, который будто создан для питонистов. Я уверен, что этот год будет счастливым для моей и вашей IT карьеры =)
P.S. Надеюсь, Deus Mechanicus из Вахи 40k направит наш код в нужное русло.
Ссылки к статье
Динамический полиморфизм с использованием std::variant и std::visit
Все статьи