Cat

Сравнение улучшения кода в Python и C++ (часть 3)

Данная статья посвящена:

  • Работе с дебаггером gdb (GNU Debugger) на примере неисправного генератора на C++;
  • Открытию новой рубрики "Python и C++ в Open source";
  • Окончанию работы над телефонной книгой.

Реклама

Icon Link
Сравнение Python и С/C++ Arduinum628 15 Январь 2025 Просмотров: 63

Вступление

Всем доброго дня! В предыдущей статье Сравнение улучшения кода в Python и C++ (часть 2) из рубрики "Сравнение Python и C/C++" мы улучшили наш код Python версии телефонной книги.

Мы поговорили о статической типизации C++ как аналоге аннотаций Python, улучшили код, сделали его более удобным и переносимым. Сегодня я хотел внедрить декораторы, обработать ошибки и написать генератор для нашей телефонной книги на C++. Однако будет много неожиданных тем в моей статье. Она будет посвящена работе с дебаггером gdb (GNU Debugger), открытию новой рубрики и завершению работы над телефонной книгой.

Для тех, кому интересна тема gdb на практике, можете попробовать запустить код с его помощью и вводить команды, а все остальные могут просто почитать статью для общего развития. Поехали!

 

Генераторы

Генераторы создают последовательности значений, которые могут быть возвращены по одному без необходимости загружать всё сразу в память. Реализация телефонной книги на языке Python имела генераторную функцию get_all_contacts(), которая проходила по словарю с контактами и возвращала генератор со строкой контакта.

 

Функция get_all_contacts

Она выглядела вот так:

@catching_exceptions
@print_items
def get_all_contacts(phone_book: dict[str: str]) -> Generator[str, str, str]:
    """Генераторная функция для возврата всех контактов"""

    type_phone_book_valid(phone_book=phone_book)

    for contact, number in phone_book.items():
        yield f'{contact} - {number}'

 

Сейчас я покажу вам пример генератора на C++, который будет неисправен. Это необходимо для работы дебаггера gdb.

#include <iostream>
#include <unordered_map>
#include <string>
#include <coroutine>


// Структура контакта
struct Contact {
    std::string name;
    std::string phone;
};

// Структура генератора контакта
struct ContactGenerator {
    struct promise_type {
        Contact current_contact;

        auto get_return_object() {
            return ContactGenerator{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { 
            return {}; 
        }
        std::suspend_always final_suspend() noexcept { 
            return {}; 
        }
        void unhandled_exception() { 
            std::terminate(); 
        }

        std::suspend_always yield_value(Contact contact) {
            current_contact = std::move(contact);
            return {};
        }

        void return_void() {}
    };

    using handle_type = std::coroutine_handle<promise_type>;

    explicit ContactGenerator(handle_type h) : handle_(h) {}
    ~ContactGenerator() { 
        if (handle_) handle_.destroy(); 
    }

    Contact next() {
        handle_.resume();
        return handle_.done() ? Contact{"", ""} : handle_.promise().current_contact;
    }

    bool hasNext() {
        return !handle_.done();
    }

private:
    handle_type handle_;
};

// Генераторная функция для возврата всех контактов
ContactGenerator getAllContacts(const std::unordered_map<std::string,
                                std::string>& phoneBook) {
    for (const auto& contact : phoneBook) {
        co_yield Contact{contact.first, contact.second};
    }
}

// Главная функция
int main() {
    std::unordered_map<std::string, std::string> phoneBook = {
        {"Николай Николаев", "+72423535354"}, 
        {"Виталий Краснов", "+84424434345"}
    };

    // код для проверки работы генератора (временный)
    auto generator = getAllContacts(phoneBook);
    while (generator.hasNext()) {
        auto contact = generator.next();
        if (!contact.name.empty()) {  // Проверка на пустое значение
            std::cout << contact.name << " - " << contact.phone << std::endl;
        }
    }

    return 0;
}

 

Скомпилируем наш код, чтобы проверить его:

 $ g++ -o phone_book_cpp phone_book.cpp

In file included from phone_book.cpp:4:
/usr/include/c++/12/coroutine:361:2: error: #error "the coroutine header requires -fcoroutines"
  361 | #error "the coroutine header requires -fcoroutines"
      |  ^~~~~
phone_book.cpp:66:14: error: ‘suspend_always’ in namespace ‘std’ does not name a type
   66 |         std::suspend_always initial_suspend() {
      |              ^~~~~~~~~~~~~~
phone_book.cpp:69:14: error: ‘suspend_always’ in namespace ‘std’ does not name a type
   69 |         std::suspend_always final_suspend() noexcept {
      |              ^~~~~~~~~~~~~~
phone_book.cpp:76:14: error: ‘suspend_always’ in namespace ‘std’ does not name a type
   76 |         std::suspend_always yield_value(Contact contact) {
      |              ^~~~~~~~~~~~~~
phone_book.cpp:84:30: error: ‘coroutine_handle’ in namespace ‘std’ does not name a template type
   84 |     using handle_type = std::coroutine_handle<promise_type>;
      |                              ^~~~~~~~~~~~~~~~
phone_book.cpp:86:42: error: expected ‘)’ before ‘h’
   86 |     explicit ContactGenerator(handle_type h) : handle_(h) {}
      |                              ~           ^~
      |                                          )
phone_book.cpp:102:5: error: ‘handle_type’ does not name a type
  102 |     handle_type handle_;
      |     ^~~~~~~~~~~
phone_book.cpp: In member function ‘auto ContactGenerator::promise_type::get_return_object()’:
phone_book.cpp:63:22: error: ‘coroutine_handle’ is not a member of ‘std’
   63 |                 std::coroutine_handle<promise_type>::from_promise(*this)
      |                      ^~~~~~~~~~~~~~~~
phone_book.cpp:63:54: error: ‘::from_promise’ has not been declared
   63 |                 std::coroutine_handle<promise_type>::from_promise(*this)
      |                                                      ^~~~~~~~~~~~
phone_book.cpp:62:36: error: expected primary-expression before ‘{’ token
   62 |             return ContactGenerator{
      |                                    ^
phone_book.cpp:62:36: error: expected ‘;’ before ‘{’ token
phone_book.cpp:63:22: error: ‘coroutine_handle’ is not a member of ‘std’
   63 |                 std::coroutine_handle<promise_type>::from_promise(*this)
      |                      ^~~~~~~~~~~~~~~~
phone_book.cpp:63:51: error: expected primary-expression before ‘>’ token
   63 |                 std::coroutine_handle<promise_type>::from_promise(*this)
      |                                                   ^
phone_book.cpp:63:54: error: ‘::from_promise’ has not been declared
   63 |                 std::coroutine_handle<promise_type>::from_promise(*this)
      |                                                      ^~~~~~~~~~~~
phone_book.cpp: In destructor ‘ContactGenerator::~ContactGenerator()’:
phone_book.cpp:88:13: error: ‘handle_’ was not declared in this scope
   88 |         if (handle_) handle_.destroy();
      |             ^~~~~~~
phone_book.cpp: In member function ‘Contact ContactGenerator::next()’:
phone_book.cpp:92:9: error: ‘handle_’ was not declared in this scope
   92 |         handle_.resume();
      |         ^~~~~~~
phone_book.cpp: In member function ‘bool ContactGenerator::hasNext()’:
phone_book.cpp:97:17: error: ‘handle_’ was not declared in this scope
   97 |         return !handle_.done();
      |                 ^~~~~~~
phone_book.cpp: In function ‘ContactGenerator getAllContacts(const std::unordered_map<std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> >&)’:
phone_book.cpp:109:9: error: ‘co_yield’ was not declared in this scope; did you mean ‘sched_yield’?
  109 |         co_yield Contact{contact.first, contact.second};
      |         ^~~~~~~~
      |         sched_yield
phone_book.cpp:111:1: warning: no return statement in function returning non-void [-Wreturn-type]
  111 | }
      | ^

 

Ошибка error: #error "the coroutine header requires -fcoroutines" возникла потому, что поддержка корутин появилась в стандарте C++20. Корутина является специальной функцией, которая способна приостановить и возобновить своё выполнение.

Многие из читателей могут подумать об асинхронных функциях, когда слышат слово корутина. Асинхронная функция - это функция, которая выполняется асинхронно, то есть ее выполнение не блокирует выполнение других операций. В языке Python мы можем ждать асинхронную функцию (корутину) с помощью await. Однако в нашем коде у нас нет асинхронных функций, а корутиной является генераторная функция getAllContacts().

Ключевое слово co_yield используется для создания генераторной функции так же, как и ключевое слово yield в Python. Ключевое слово co_yield позволяет функции приостановить свое выполнение и вернуть временное значение вызывающему коду, сохраняя свое состояние для последующего продолжения. Наша функция приостанавливает выполнение на каждом вызове co_yield и возобновляет выполнение на каждом вызове метода next(), который получает следующее значение генератора.

Давайте добавим стандарт C++20 в команду компиляции, который необходим для работы корутин:

$ g++ -std=c++20 -fcoroutines -o phone_book_cpp phone_book.cpp

 

Как видим, наш код скомпилировался без ошибок. Ключ -std=c++20 добавляет поддержку стандарта C++20. Ключ -fcoroutines добавляет в компилятор поддержку корутин.

Теперь давайте запустим наш код:

$ ./phone_book_cpp

Виталий Краснов - 84424434345
�иколай Николаев - 72423535354
Ошибка сегментирования (образ памяти сброшен на диск)

 

У нас получился очень странный вывод для программы, которая успешно скомпилировалась. Сейчас нужно постараться разобраться с ошибкой и убрать этих мелких жучков =)

 

Использование отладчика кода

Слово �иколай с символом вместо буквы Н говорит о том что у нас проблемы с кодировкой для кириллицы. Ошибка сегментирования может говорить о том, что у нас проблема работы с памятью. Для того чтобы точно разобраться с этим, нам нужно использовать gdb.

gdb (GNU Debugger) - это мощный отладчик для программ, написанных на языке C, C++, и других. Он позволяет разработчикам исследовать выполнение программы, выявлять и устранять ошибки, а также анализировать причины сбоев.

Для того чтобы воспользоваться отладчиком, нам необходимо скомпилировать программу с отладочной информацией. Для этого добавим ключ -g в команду компиляции.

$ g++ -std=c++20 -fcoroutines -g -o phone_book_cpp phone_book.cpp

 

Теперь нам нужно запустить программу с отладчиком кода:

gdb ./phone_book_cpp
GNU gdb (Debian 13.1-3) 13.1
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
--Type <RET> for more, q to quit, c to continue without paging--

 

Когда мы запускаем программу с отладчиком gdb, первым делом видим информацию о нём. Кратко о том, что у нас на экране:

  • Строка GNU gdb (Debian 13.1-3) 13.1 показывает название и версию отладчика.
  • Строка License GPLv3+ это лицензия.
  • Строка с "x86_64-linux-gnu это наша архитектура.
  • Ссылка <https://www.gnu.org/software/gdb/bugs/> нужна для отчётов о багах.

Далее нажимаем enter для того, чтобы начать вводить команды в gdb.

Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./phone_book_cpp...
(gdb)

 

Сейчас давайте запустим программу в отладчике:

(gdb) run

Starting program: /home/user_name/Documents/Сode improvement/phone_book_cpp 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Виталий Краснов - 84424434345
�ван Николаев - 72423535354

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7ab7f15 in arena_for_chunk (ptr=0x555555576170) at ./malloc/arena.c:156
156     ./malloc/arena.c: Нет такого файла или каталога.

 

Мы видим, что у нас запустилась программа и мы видим её вывод. Также мы уже видим более подробную информацию об ошибке программы. Адрес 0x00007ffff7ab7f15 указывает на место в памяти, где произошла ошибка сегментирования, а именно на функцию arena_for_chunk. Мы также видим, в каком файле и на какой строке произошла ошибка: ./malloc/arena.c:156.

Хотя мы уже понимаем, где находится ошибка, мы не знаем, что такое arena.c. Она является частью GNU C Library (glibc), и её задача - работа с памятью и её освобождение.

Теперь нам нужно, находясь в отладчике, посмотреть стек вызовов. Стек вызовов (или backtrace) - это структура данных, используемая для хранения информации о последовательности вызовов функций, которые привели программу к текущему месту выполнения. Когда программа вызывает функцию, информация о вызове помещается в стек вызовов, а когда функция завершается, эта информация удаляется из стека.

Давайте посмотрим как программа попала в текущее состояние:

(gdb) bt

#0  0x00007ffff7ab7f15 in arena_for_chunk (ptr=0x555555576170) at ./malloc/arena.c:156
#1  arena_for_chunk (ptr=0x555555576170) at ./malloc/arena.c:160
#2  __GI___libc_free (mem=<optimized out>) at ./malloc/malloc.c:3384
#3  0x000055555555c1fd in std::__new_allocator<char>::deallocate (this=0x555555576150, __p=0x555555576180 "", __n=3761407512569721913) at /usr/include/c++/12/bits/new_allocator.h:158
#4  0x000055555555ae7f in std::allocator<char>::deallocate (__n=3761407512569721913, __p=0x555555576180 "", this=0x555555576150) at /usr/include/c++/12/bits/allocator.h:200
#5  std::allocator_traits<std::allocator<char> >::deallocate (__a=..., __p=0x555555576180 "", __n=3761407512569721913) at /usr/include/c++/12/bits/alloc_traits.h:496
#6  0x000055555555a0d0 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_destroy (this=0x555555576150, __size=3761407512569721912)
    at /usr/include/c++/12/bits/basic_string.h:292
#7  0x000055555555990a in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_dispose (this=0x555555576150) at /usr/include/c++/12/bits/basic_string.h:286
#8  0x0000555555558c10 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string (this=0x555555576150, __in_chrg=<optimized out>)

 

Понятно, что такой вывод довольно сложен для восприятия человеком, который видит подобное в первый раз. В любом случае, это полезная информация, которую можно проверить как минимум в интернете. Если говорить вкратце, программа пытается освободить память и не может этого сделать. Причины могут быть в неправильном использовании указателей, которые могут указывать на место в памяти, которого уже не существует.

 

Пора остановиться

Мы подошли к моменту, когда нужно остановиться и не использовать данный громоздкий генератор в своей программе, который только усложняет простую телефонную книгу! Я не буду пояснять, как он работает, так как он сейчас нам не нужен, бесполезен и частично неисправен. Кроме того, обязательно нужно избегать сложных и бесполезных вещей в программировании, которые тратят наше время и не несут пользы коду и программе. Хранить телефонную книгу лучше в базе данных и получать значения из неё. При запросе к базе данных легко получить все записи, поэтому изощрения с генератором это лишнее. В будущих статьях я постараюсь раскрыть тему генераторов на C++, а сейчас временно закрою эту тему, не раскрытой до конца.

Подумав немного, я решил, что телефонная книга не несёт ничего полезного как проект (кроме самого обучения), и прекратить работу над ней. Я клоню к тому, что эту телефонную книгу не будут использовать, и она просто является частью статей для обучения. Это не значит, что рубрика "Сравнение Python и C/C++" исчезнет навсегда. Мы ещё не посмотрели, чем отличаются программирование с использованием классов и многое другое. Поэтому говорить о полном закрытии рубрики пока рано. Данная рубрика теперь будет посвящена отдельным простым блокам кода, а не попыткам написать одно и то же на примере большого проекта на двух языках.

 

Анонс на следующие статьи

Вы, наверное, заметили, что языки C/C++ гораздо более объёмные, сложные, нуждающиеся в более чёткого контроля за ресурсами, чем Python. Поэтому повторять всё, что мы написали на Python, на языках C/C++ может быть очень утомительно, а иногда и гораздо сложнее. Поэтому я решил написать open source приложение, часть которого будет написана на Python, а другая часть на C++. Мы будем использовать плюсы каждого языка и сделаем так, чтобы они работали вместе. Например, язык Python хорошо подойдёт для веб-разработки или разработки ботов в качестве их основы. Язык же C++ хорош для вычислений, нейросети на нём работают гораздо быстрее, системные утилиты можно писать для бекапа.

Что за проект я буду писать? Пока я не буду раскрывать подробностей всей идеи проекта, но вкратце расскажу, чем я займусь. Это будет open source проект, который будет доступен каждому человеку. Возможно, что даже кто-то захочет помочь его улучшить, написав что-то для его модернизации. Это будет доступно после того, как я напишу минимально жизнеспособную первую версию. Пока могу сказать лишь то, что софт будет связан со здоровьем человека и его привычками. Более подробно о проекте вы узнаете в следующей статье, где я напишу основу для проекта и расскажу о нём более детально.

Следующая статья выйдет 06.02.25, которая будет открытием новой рубрики "Python и C++ в Open source". В первой статье новой рубрики я начну писать основу софта для здоровых привычек человека на языке Python.

Насчёт рубрики "Вхожу в IT", в которой я пишу бот-магазин, могу сказать, что работа над ботом продолжается. В данный момент пишу заполнение базы данных магазина через документ Excel. Можно сказать, что написание проекта немного затянулось, но зато я много полезного извлёк при работе с ботом и поделюсь с вами своими мыслями в 3-й части статьи. Далее будет ещё интереснее =)

 

Заключение

  1. Научились работать с дебаггером gdb (GNU Debugger);
  2. Увидели пример неисправного генератора на C++;
  3. Поняли, когда лучше остановится в работе над проектом;

Автор

    Нет комментариев

    Реклама