Elektroda.pl
Elektroda.pl
X
Please add exception to AdBlock for elektroda.pl.
If you watch the ads, you support portal and users.

Język programowania C++ - w kilku słowach o wyjątkach.

_lazor_ 04 Jun 2022 19:17 2322 1
  • Język programowania C++ - w kilku słowach o wyjątkach.
    Języki programowania oferują programiście szereg udogodnień, które mają na celu zwiększenie niezawodności, poprawę czytelności czy zmniejszenie złożoności pisanego kodu. Niestety kod pisany w wysokopoziomowym języku nie jest docelowym produktem programisty, jest nim kod wykonywalny przez maszynę, który spełnia założenia programu.
    Osobiście wychodzę z założenia, że język programowania jest jedynie narzędziem - trochę bardziej rozbudowanym młotkiem. Nie sztuką jest machać młotkiem, sztuką jest tak go użyć aby wbić gwóźdź i nie trafić się w palec. Jednak gdy przyszło by nam używać tego samego młotka aby coś wyrzeźbić to byłby problem. Tak też postrzegam język programowania. Wiemy, że pisać na klawiaturze potrafi praktycznie każdy, ale informacja o tym kto jakie zadania potrafi zrealizować nie jest już taka oczywista. Weźmy wykorzystanie funkcjonalności młotka, jaką jest wbijanie gwoździa. Czyż to nie jest trywialne? Cóż, kiedyś natknąłem się na materiał szerzej opisujący wbijanie gwoździ w drewno, jak się okazuje nie jest to często tak banalne, a potrzebna wiedza i doświadczenie może zaskoczyć.
    Dla mnie tak samo wygląda wykorzystanie funkcjonalności języka. Niby jest opis działania, podana składnia, więc co w tym trudnego by z nich korzystać?
    W niniejszym materiale chciałbym na bazie architektury ARM cortex-m4, toolchaina gcc-arm-none-eabi w języku C++, przedstawić, co kryje się za dość niewinnie wyglądającą funkcjonalnością „exceptions”.

    Zaczniemy od ABI i C++. Czym jest ABI? Jest to specyfikacja wszystkich szczegółów niskiego poziomu, które umożliwiają współpracę oddzielnie skompilowanych modułów. Gdy piszemy składnie wyjątków to kompilator bardzo uproszczając zamieni je na poniższe rutyny:
    Code: c
    Log in, to see the code


    Dzięki ujednoliceniu C++ ABI (rutyny zaczynające się od __cxa__ ) nie jest istotne czy będziemy pisać na PC czy cortex-m powyższe rutyny będą wołane aby obsłużyć wyjątek. Jednak to jest jedynie deklaracja, sama implementacja jest zawarta w GNU C++ library (lub w innej dystrybucji). Jezyk C nie zawiera oficjalnego ABI i głównie opiera się na lokalnym ABI hardware (ARM eabi32). Tylko, co ma C++ exceptions do języka C? Tutaj przechodzimy do API i wywołania systemowych (syscalls). Bardzo w skrócie jest wykorzystywany model unixopodobny, co oznacza, że syscall są częścią standardowej biblioteki języka C, a że implementacja wyjątków korzysta z syscalls to również zależy od implementacji biblioteki standardowej C. W arm none eabi jest wspierana biblioteka newlib, która ma kilka wersji. Możemy wybrać jedną z nich dzięki parametrowi -specs= który może przyjąć wartość:

    rdimon.specs - wersja dla częściowo hostowym środowisku, z implementacją syscalls.
    nosys.specs – wersja dla freestanding, ale z protezami dla wywołań systemowych, gdzie zostają zwrócone błędy, jeśli zostaną wywołane
    nano.spec – Najbardziej okrojona wersja newlib (newlib-nano), nie posiada w ogóle wielu implementacji wywołań systemowych.

    Każda z powyższych specyfikacji linkuje do projektu inne implementacje standardowej biblioteki C.
    Jeśli chcemy użyć wyjątków w takiej postaci, jakie oferuje toolchain arm-none-eabi to proponuje użyć specyfikacji nosys, co niestety ma swoje konsekwencje w rozmiarze pliku binarnego (72KB), próba wykorzystania specyfikacji nano, powoduje braki dla symboli _exit, _sbrk, _kill i _getpid i trzeba je samodzielnie dostarczyć (czy to przez kompilacje własnoręcznie biblioteki standardowej czy dolinkowanie tych symboli z innego źródła). W takim wypadku uzyskałem rozmiar pliku binarnego około 10KB, chociaż przyznam, że w tej wersji i tak wyjątki nie działały i wymagałoby to głębszego grzebania, czego już nie chciałem robić, ale są ludzie, którym się chciało i udostępnili swoje wyniki pracy:
    https://github.com/tgree/docker-gcc-arm-none-eabi-nano_eh-build


    Jeśli chodzi o wydajność to przy poprawnym użyciu wyjątków mają one pozytywny wpływ na wydajność wykonywanego kodu (w stosunku do errno). Ale czym jest poprawne użycie wyjątków? Cóż jakby to powiedzieć… nie wywoływać wyjątków. Wyjątek ma się wywołać jak nazwa wskazuje, wyjątkowo, co w praktyce znaczy nigdy. Jeśli ktoś chce potraktować wyjątek, jako pewną formę „if’a”, który w realnym i prawidłowym scenariuszu programu może być wywołany to niestety źle używa tej koncepcji. W takim wypadku wpływ na wydajność jest znaczący.

    Nieumiejętne wykorzystanie wyjątków może również doprowadzić do kilku naprawdę poważnych problemów. Jednym z problemów może być wywołanie terminte(), w świecie standalone embedded to może być naprawdę spory problem i być undefined behviour.
    Innym problemem może być wyciek pamięci. Alokując dynamicznie pamięć korzystając z „gołego” pointera, może dojść do wywołania wyjątku i wyjścia programu poza zakres życia takiego pointera bez jego zwolnienia, co doprowadzi do wycieku. Cóż, dlatego zalecane jest używanie unique_ptr. Gdy zostanie wywołany wyjątek to zostanie również wywołany destruktor unique_ptr a dzięki temu nie dojdzie do wycieku pamięci.

    Przez to że może dochodzić do dość nieoczekiwanych komplikacji podczas wykorzystania wyjątków, powstało zagadnienie: „Exception safety”. Chociaż poprawne wykorzystanie wyjątków zakłada, że praktycznie nigdy nie zostanie ono wywołane to jednak przyjmuje się najgorszy przypadek i trzeba przeanalizować wpływ wywołania wyjątku na dane, przebieg programu czy wystąpienie side effectów.


    Jak widać aby użyć bezpiecznie oraz poprawnie wyjątków to trzeba posiadać sporo wiedzy i doświadczenia. Chce tutaj zwrócić uwagę wielu użytkownikom, że poznanie funkcjonalności języka aby poprawnie i bezpiecznie jej używać, nie jest tożsame z poznanie składni tej funkcjonalności. Największym błędem jest używanie na siłę funkcjonalności bez zadania sobie pytania: dlaczego mam go użyć? Jaki z tego będzie uzysk? Jakie wprowadzam zagrożenia? W końcu cpp wspiera pointery, reinterpret_cast czy makra preprocesora, a ich użycie jest (i to słusznie) uważane za niepotrzebne a wręcz szkodliwe. Warto zawsze się zastanowić, jaki był cel powstania danej funkcjonalności i zastanowić się czy naprawdę wiem jak jej użyć i jakie niesie za sobą konsekwencje.




    Ciekawe linki rozwijające bardziej temat wyjątków:

    https://monkeywritescode.blogspot.com/p/c-exceptions-under-hood.html#chapter_n_2
    https://maskray.me/blog/2020-12-12-c++-exception-handling-abi
    https://www.oracle.com/technical-resources/articles/it-infrastructure/stable-cplusplus-abi.html#c_abi
    https://thephd.dev/to-save-c-we-must-save-abi-fixing-c-function-abi
    https://www.stroustrup.com/except89.pdf

    Cool? Ranking DIY
    About Author
    _lazor_
    Moderator of Designing
    Offline 
    Materiały video na temat energoelektroniki:
    https://www.youtube.com/user/sambenyaakov/videos
    Has specialization in: Programista embedded/ elektrotechnik
    _lazor_ wrote 3641 posts with rating 1058, helped 247 times. Live in city Wrocław. Been with us since 2016 year.
  • #2
    khoam
    Level 42  
    Osobiście nie stosuje wyjątków w C++. Można się zupełnie bez nich obejść, bez szkody dla:
    _lazor_ wrote:
    wpływ na wydajność wykonywanego kodu

    Uważam, że korzyści jakie potencjalnie daje obsługa wyjątków są "równoważone" przez dodatkowe problemy, jakie one wnoszą. Oczywiście mam na myśli programowanie embedded.

    Polecam inny artykuł Bjarne Stroustrup: Link

    _lazor_ wrote:
    W końcu cpp wspiera pointery, reinterpret_cast czy makra preprocesora, a ich użycie jest (i to słusznie) uważane za niepotrzebne a wręcz szkodliwe.

    W 100% sie z tym zgadzam :)