Kursy • Poradniki • Inspirujące DIY • Forum
ClangFormat – automatyczne formatowanie kodu
Popularny ClangFormat to narzędzie, które pomaga programistom zadbać o poprawne formatowanie kodu. Docenią je osoby, które musiały kiedyś analizować niesformatowany kod, oraz wszyscy ci, którzy brali udział w różnych programistycznych dyskusjach. Czy nawias klamrowy powinien znaleźć się w nowej linii czy w tej samej co nazwa funkcji? Czy podczas deklaracji wskaźników znak gwiazdki powinien znaleźć się przy typie zmiennej czy przy nazwie wskaźnika?
Są to drobiazgi, które nie wpływają na działanie programu, jednak podczas tworzenia programów warto zadbać o jednolite i czytelne formatowanie.
ClangFormat to zestaw narzędzi zbudowanych na bazie LibFormat. Może on być używany podczas pracy z kodem pisanym w C, C++, Java, JavaScript, Objective-C, Protobuf i C#. ClangFormat pozwala na stosowanie predefiniowanych stylów formatowania, np. google, chromium, mozilla, webkit, microsoft.
Możliwe jest też wykorzystanie własnego pliku konfiguracyjnego – za jego pomocą można dostosować narzędzia do naszych potrzeb i przyzwyczajeń. Przykładowy plik konfiguracyjny wygląda następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
Language: Cpp BasedOnStyle: WebKit AccessModifierOffset: '-2' AlignAfterOpenBracket: Align AlignConsecutiveMacros: 'true' AlignConsecutiveAssignments: 'false' AlignConsecutiveDeclarations: 'false' AlignEscapedNewlines: Left AlignOperands: 'false' AlignTrailingComments: 'true' AllowAllConstructorInitializersOnNextLine: 'true' AllowAllParametersOfDeclarationOnNextLine: 'true' AllowShortCaseLabelsOnASingleLine: 'false' AllowShortFunctionsOnASingleLine: Empty AllowShortIfStatementsOnASingleLine: Never AllowShortLambdasOnASingleLine: Inline AllowShortLoopsOnASingleLine: 'false' AlwaysBreakAfterReturnType: None AlwaysBreakTemplateDeclarations: 'Yes' BinPackArguments: 'false' BinPackParameters: 'false' BreakBeforeBraces: Custom BraceWrapping: AfterCaseLabel: true AfterClass: true AfterControlStatement: true AfterEnum: true AfterFunction: true AfterNamespace: true AfterObjCDeclaration: true AfterStruct: true AfterUnion: true AfterExternBlock: true BeforeCatch: true BeforeElse: true SplitEmptyFunction: false SplitEmptyRecord: true SplitEmptyNamespace: true BreakBeforeTernaryOperators: 'true' BreakConstructorInitializers: AfterColon ColumnLimit: '175' CompactNamespaces: 'true' ConstructorInitializerAllOnOneLineOrOnePerLine: 'true' Cpp11BracedListStyle: 'true' DerivePointerAlignment: 'false' FixNamespaceComments: 'true' IncludeBlocks: Merge IndentCaseLabels: 'true' IndentWidth: '4' KeepEmptyLinesAtTheStartOfBlocks: 'false' NamespaceIndentation: All MaxEmptyLinesToKeep: 1 PointerAlignment: Left ReflowComments: 'true' SortIncludes: 'true' SortUsingDeclarations: 'true' SpaceAfterCStyleCast: 'false' SpaceAfterLogicalNot: 'false' SpaceAfterTemplateKeyword: 'false' SpaceBeforeAssignmentOperators: 'true' SpaceBeforeCpp11BracedList: 'true' SpaceBeforeCtorInitializerColon: 'true' SpaceBeforeInheritanceColon: 'true' SpaceBeforeParens: ControlStatements SpaceBeforeRangeBasedForLoopColon: 'true' SpaceInEmptyParentheses: 'false' SpacesBeforeTrailingComments: '3' SpacesInAngles: 'false' SpacesInCStyleCastParentheses: 'false' SpacesInContainerLiterals: 'false' SpacesInParentheses: 'false' SpacesInSquareBrackets: 'false' SpaceInEmptyBlock: 'false' Standard: Cpp11 TabWidth: '4' |
Jak zainstalować ClangFormat?
Osoby korzystające z Debiana (i podobnych systemów) zainstalują program za pomocą jednej linijki:
1 |
$ sudo apt-get install clang-format |
Osoby, które korzystają z Windowsa, mogą np. zainstalować LLVM. W trakcie instalacji tego programu trzeba zadbać o to, aby zaznaczona była opcja dodania LLVM do systemowej zmiennej PATH.
Czasami może się zdarzyć, że pomimo zaznaczenia tej opcji wpis do zmiennej środowiskowej PATH nie zostanie dodany. Warto więc sprawdzić to ręcznie po instalacji programu. W tym celu otwieramy menu start, a następnie w wyszukiwarkę systemową wpisujemy „Edytuj zmienne środowiskowe”. Pojawi się nowe okno – klikamy tam przycisk „Zmienne środowiskowe”, a następnie na liście rozwijanej, która opisana jest jako „Zmienne systemowe”, wybieramy wpis „Path” i klikamy przycisk edycji. Jeśli nie ma tam wpisu dotyczącego LLVM, to dodajemy go ręcznie, np.: „C:\Program Files\LLVM\bin”.
Jak korzystać z ClangFormat?
Korzystanie z programu ogranicza się właściwie do wydania jednego polecenia. Zanim do tego dojdzie, musimy jednak umieścić plik konfiguracyjny w katalogu głównym danego projektu. Może to być nasza konfiguracja lub jeden z dostępnych formatów, który został wspomniany już wcześniej.
Zakładając, że chcemy sformatować plik main.cpp, wywołujemy:
1 |
$ clang-format -i main.cpp |
Oczywiście przed wydaniem tego polecenia musimy przejść w konsoli do katalogu, w którym znajduje się nasz projekt. W tym celu na Linuksie i Windowsie możemy posługiwać się poleceniem cd, np.:
1 |
$ cd “C:\Users\Mateusz\Desktop\CppCheck_Test” |
To już jest właściwie wszystko. Natychmiast po wydaniu tego polecenia kod zostanie sformatowany zgodnie z dostarczoną konfiguracją. Osoby korzystające z Linuksa mogą też w łatwy sposób uruchomić narzędzie dla całego projektu. W tym celu w głównym katalogu projektu wystarczy wydać polecenie:
1 2 |
$ find . -iname *.h -o -iname *.c -o -iname *.cpp -o -iname *.hpp \ | xargs clang-format -style=file -i -fallback-style=none |
Efekt działania ClangFormat (zintegrowanego ze środowiskiem) wygląda następująco:
Jeśli stosujemy własny plik konfiguracyjny, to trzeba pamiętać, że ClangFormat obsługuje konfigurację zapisaną w formacie, który nazywany jest „UTF-8 (without BOM)”. Jeśli nasz plik konfiguracyjny będzie źle zakodowany, to otrzymamy błąd, który będzie wyglądał następująco:
1 2 |
YAML:1:4: error: Got empty plain scalar ■--- |
Aby uniknąć tego problemu, trzeba zapisać plik z odpowiednim kodowaniem znaków – można to zrobić np. za pomocą Notepad++ lub Visual Studio Code.
Jak zintegrować ClangFormat z IDE?
Wywoływanie narzędzia ClangFormat jest znacznie wygodniejsze, jeśli zintegrujemy je bezpośrednio z IDE, w którym piszemy programy. Większość popularnych edytorów posiada już taką możliwość.
Na przykład w przypadku Visual Studio Code można zainstalować gotowe rozszerzenie Clang-Format. Gdy jest ono dodane do IDE, wystarczy, że otworzymy plik ustawień użytkownika (Ctrl + Shift + P). Następnie wpisujemy „Preferences: Open Settings”, z listy wybieramy „Preferences: Open Settings (JSON)” i dopisujemy do pliku następującą zawartość:
1 2 3 4 5 6 7 |
{ "clang-format.executable": "C:/Program Files/LLVM/bin", "[cpp]": { "editor.defaultFormatter": "ms-vscode.cpptools", "editor.formatOnSave": true } } |
ClangFormat – jak generować własne konfiguracje?
Warto poświęcić trochę czasu, aby przygotować swoją konfigurację formatowania, zgodnie z którym zadziała ClangFormat – tutaj pomocna będzie strona, na której opisano wszystkie parametry. Można też wykorzystać gotowe style, które są już wbudowane w to narzędzie. Konfigurację dla danego stylu można wygenerować za pomocą poniższego polecenia (trzeba tylko pamiętać o kodowaniu pliku):
1 |
$ clang-format.exe -style=llvm -dump-config > .clang-format |
Liczba dostępnych parametrów pliku konfiguracyjnego może przytłaczać. Na szczęście nie trzeba tego wszystkiego edytować ręcznie. Można wspomóc się interaktywnym edytorem online, dzięki któremu możliwe jest obserwowanie wszystkich zmian „na żywo”.
Cppcheck – analiza błędów w kodzie
Pierwszym narzędziem, które wskaże błędy w kodzie, jest kompilator. Jeśli napiszemy kod, który nie jest zgodny z danym standardem, to otrzymamy błąd kompilacji. Można też napisać kod, który nie narusza standardu, ale może spowodować tzw. zachowanie niezdefiniowane (ang. undefined behaviour).
Tu przyda się Cppcheck, który potrafi wykonywać analizę statyczną kodu C/C++. Program ten analizuje nieskompilowany kod pod kątem tego, czy występują tam miejsca, które można potencjalnie określić jako mało bezpieczne. Co ciekawe, Cppcheck został zaprojektowany, aby móc analizować kod C/C++, nawet jeśli ma on niestandardową składnię, jak np. w projektach embedded.
Jak zainstalować Cppcheck?
Podobnie jak w poprzednim przykładzie, użytkownicy Linuksa mogą wydać jedno polecenie:
1 |
$ sudo apt-get install cppcheck |
Dla Windowsa konieczne jest pobranie instalatora ze strony projektu. Następnie należy dodać ręcznie odpowiednią ścieżkę do zmiennej środowiskowej PATH. Proces ten przebiega tak samo jak w przypadku opisanego wcześniej ClangFormat – tym razem jednak zmienna ma wskazywać na ścieżkę Cppcheck, czyli np.: „C:\Program Files\Cppcheck”.
Jak korzystać z Cppcheck?
Korzystanie z Cppcheck jest również bardzo proste, ale tutaj podczas pierwszych testów przyda się jakiś przykładowy kod programu. Posłużmy się więc takim fragmentem:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <cstdio> int main() { int* ptr = nullptr; int a = 5; int b; auto c = new int {5}; if (b > 0) { std::puts("b greater than 0"); } else { std::puts("b less than 0"); } return *ptr + a; } |
Czy ten kod jest poprawny? Spróbujmy skompilować taki program. Oczywiście potrzebny do tego jest kompilator. Użytkownicy Linuksa mogą go zainstalować za pomocą poniższej linijki, a osoby pracujące na Windowsie powinny mieć już ten kompilator, bo został zainstalowany razem z LLVM.
1 |
$ sudo apt-get install g++ |
Kompilację na Ubuntu wykonujemy za pomocą wywołania:
1 |
$ g++ main.cpp |
A na Windowsie przy użyciu LLVM:
1 |
$ clang++ main.cpp |
Kolejne wywołania przedstawione zostaną za pomocą g++, jednak będą one tak samo działać w przypadku clang++ na Windowsie, gdy zmienimy g++ na clang++. Niezależnie od używanego systemu kompilator nie znajdzie w tym kodzie żadnych błędów, więc w katalogu z kodem źródłowym pojawi się nasz program, który na Linuksie będzie nazywał się a.out, a na Windowsie a.exe.
Taki program można już uruchamiać jak każdy inny. Na Ubuntu:
1 2 3 4 |
$ ./a.out b greater than 0 Segmentation fault (core dumped) |
Na Windowsie:
1 2 3 |
$ ./a.exe b greater than 0 |
W wyjściu konsoli w przypadku Ubuntu możemy zobaczyć błąd „Segmentation fault (core dumped)”, czyli naruszenie pamięci i wkroczenie do akcji systemu, który wyłączył nasz program, zanim ten narobił bałaganu. W przypadku Windowsa nie dostajemy jednak żadnej informacji o naruszeniu pamięci.
Sprawdźmy, co do powiedzenia ma Cppcheck. Najpierw musimy w terminalu przejść do katalogu z tym projektem. Następnie wywołujemy poniższe polecenie (identycznie dla Ubuntu i Windowsa):
1 |
$ cppcheck --enable=all --inconclusive main.cpp |
W wyniku wywołania tego polecenia powinniśmy otrzymać następujący wynik:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
Checking main.cpp ... main.cpp:20:13: error: Null pointer dereference: ptr [nullPointer] return *ptr + a; ^ main.cpp:5:16: note: Assignment 'ptr=nullptr', assigned value is 0 int* ptr = nullptr; ^ main.cpp:20:13: note: Null pointer dereference return *ptr + a; ^ main.cpp:11:9: error: Uninitialized variable: b [uninitvar] if (b > 0) ^ main.cpp:9:12: style: Variable 'c' is assigned a value that is never used. [unreadVariable] auto c = new int {5}; ^ main.cpp:8:9: style: Variable 'b' is not assigned a value. [unassignedVariable] int b; ^ nofile:0:0: information: Cppcheck cannot find all the include files (use --check-config for details) [missingIncludeSystem] |
Otrzymaliśmy całą listę problemów z tym programem. Osoby początkujące mogą czuć się trochę zagubione, ale bardziej doświadczeni programiści wykorzystają tego typu informacje. Spróbujmy teraz poprawić ten kod. Mógłby on wyglądać następująco (jeśli taki był zamiar autora).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <cstdio> #include <iostream> int main() { int a = 5; int* ptr = &a; int b; std::cin >> b; if (b > 0) { std::puts("b greater than 0"); } else { std::puts("b less than 0"); } return *ptr + a; } |
Tym razem wskaźnikowi przypisaliśmy adres zmiennej, dzięki czemu nie dokonujemy już dereferencji wskaźnika nullptr (undefined behaviour). Następnie usunęliśmy linię, w której dynamicznie zaalokowaliśmy pamięć o rozmiarze jednego inta (i tak z niej nie korzystaliśmy).
W dalszej kolejności zmiennej b przypisujemy wartość przekazaną przez użytkownika – dzięki temu kod nie wykonuje już warunkowego skoku, który jest zależny od niezainicjowanej zmiennej. Po dokonaniu zmian i wywołaniu narzędzia nie otrzymujemy już żadnych problemów, a nasz program zadziała prawidłowo.
Poznaj więcej możliwości Cppcheck
Cppcheck możemy też oczywiście wywołać dla całego projektu, uruchamiając go w katalogu głównym projektu i podając ścieżki do nagłówków projektowych:
1 |
$ cppcheck --enable=all --inconclusive . -I <path_a> -I <path_b> |
Oczywiście przedstawione powyżej wywołanie narzędzia to wersja podstawowa. Wszystkie opcje, które są wbudowane w Cppcheck, znajdziemy w dokumentacji lub w pomocy ($ cppcheck --help). Warto też pamiętać, że program ten posiada graficznego klienta:
Valgrind – analiza dynamiczna kodu
Wspomniany wcześniej Cppcheck jest narzędziem do analizy statycznej. Oznacza to, że dokonuje ono analizy na podstawie kodu i nie wymaga w tym celu uruchomienia testowanego programu. Natomiast Valgrind jest narzędziem do analizy dynamicznej w tzw. runtimie, czyli analiza odbywa się w czasie działania programu (kosztem jego szybkości).
Valgrind pozwala m.in. na debugowanie problemów związanych z zarządzaniem pamięcią.
Jak zainstalować Valgrind?
Valgrind nie jest dostępny na Windowsa, ale istnieją alternatywne rozwiązania tego typu. W przypadku Linuksa instalacja jest oczywiście bardzo prosta i sprowadza się do jednej linijki:
1 |
$ sudo apt-get install valgrind |
Jak korzystać z Valgrinda?
Również tutaj przyda nam się jakiś kod testowy – wykorzystajmy więc dokładnie ten sam program co w przypadku eksperymentów z Cppcheck.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <cstdio> int main() { int* ptr = nullptr; int a = 5; int b; auto c = new int {5}; if (b > 0) { std::puts("b greater than 0"); } else { std::puts("b less than 0"); } return *ptr + a; } |
Aby uruchomić ten program pod Valgrindem, musimy go najpierw skompilować, i to najlepiej w trybie debug (dodając flagę g). Następnie uruchamiamy analizę dynamiczną:
1 2 |
$ g++ -g -Wall -Wextra main.cpp $ valgrind ./a.out |
W wyniku działania tego programu otrzymamy taki raport:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
==6638== Memcheck, a memory error detector ==6638== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. ==6638== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info ==6638== Command: ./a.out ==6638== ==6638== Conditional jump or move depends on uninitialised value(s) ==6638== at 0x10919C: main (main.cpp:11) ==6638== b less than 0 ==6638== Invalid read of size 4 ==6638== at 0x1091BC: main (main.cpp:20) ==6638== Address 0x0 is not stack'd, malloc'd or (recently) free'd ==6638== ==6638== ==6638== Process terminating with default action of signal 11 (SIGSEGV) ==6638== Access not within mapped region at address 0x0 ==6638== at 0x1091BC: main (main.cpp:20) ==6638== If you believe this happened as a result of a stack ==6638== overflow in your program's main thread (unlikely but ==6638== possible), you can try to increase the size of the ==6638== main thread stack using the --main-stacksize= flag. ==6638== The main thread stack size used in this run was 8388608. ==6638== ==6638== HEAP SUMMARY: ==6638== in use at exit: 4 bytes in 1 blocks ==6638== total heap usage: 3 allocs, 2 frees, 73,732 bytes allocated ==6638== ==6638== LEAK SUMMARY: ==6638== definitely lost: 0 bytes in 0 blocks ==6638== indirectly lost: 0 bytes in 0 blocks ==6638== possibly lost: 0 bytes in 0 blocks ==6638== still reachable: 4 bytes in 1 blocks ==6638== suppressed: 0 bytes in 0 blocks ==6638== Rerun with --leak-check=full to see details of leaked memory ==6638== ==6638== Use --track-origins=yes to see where uninitialised values come from ==6638== For lists of detected and suppressed errors, rerun with: -s ==6638== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0) Segmentation fault (core dumped) |
Otrzymujemy w ten sposób szereg informacji – najistotniejsze to:
1 2 |
==6638== Conditional jump or move depends on uninitialised value(s) ==6638== at 0x10919C: main (main.cpp:11) |
Zgłoszenie to mówi nam, że w programie jest warunek, który działa na podstawie niezainicjalizowanej zmiennej – rzeczywiście tak jest, zmienna b nie ma przypisanej wartości.
Druga ważna uwaga wygląda następująco:
1 2 3 |
==6638== Invalid read of size 4 ==6638== at 0x1091BC: main (main.cpp:20) ==6638== Address 0x0 is not stack'd, malloc'd or (recently) free'd |
Tłumacząc na polski, w linii 20 mamy do czynienia z niepoprawnym odczytem pamięci – rzeczywiście, w tej linii próbujemy wykonać dereferencje na wskaźniku nullptr. Następnie Valgrind informuje nas, jakim sygnałem system operacyjny zakończył nasz program:
1 2 3 |
==6638== Process terminating with default action of signal 11 (SIGSEGV) ==6638== Access not within mapped region at address 0x0 ==6638== at 0x1091BC: main (main.cpp:20) |
To jednak jeszcze nie koniec problemów – ostatnim jest brak zwolnienia pamięci, którą zaalokowaliśmy dynamicznie. Valgrind informuje nas o tym we fragmencie:
1 2 3 4 5 6 |
==6638== LEAK SUMMARY: ==6638== definitely lost: 0 bytes in 0 blocks ==6638== indirectly lost: 0 bytes in 0 blocks ==6638== possibly lost: 0 bytes in 0 blocks ==6638== still reachable: 4 bytes in 1 blocks ==6638== suppressed: 0 bytes in 0 blocks |
Dynamiczna alokacja pamięci za pomocą operatora new nie jest niczym złym, ale brak jej zwolnienia – już tak. Jeśli wywołamy Valgrinda z dwoma dodatkowymi argumentami:
1 |
$ valgrind --leak-check=full --show-leak-kinds=all ./a.out |
Otrzymamy dodatkową informację o miejscu wycieku pamięci:
1 2 3 |
==7167== 4 bytes in 1 blocks are still reachable in loss record 1 of 1 ==7167== at 0x483BE63: operator new(unsigned long) (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) ==7167== by 0x10918D: main (main.cpp:9) |
Posiadając takie informacje, jesteśmy w stanie sprawdzić poprawność naszego programu i znaleźć przyczynę ewentualnego problemu. Błędy związane z zarządzaniem pamięcią potrafią być bardzo trudne do znalezienia, więc pomoc tego typu narzędzi jest niezwykle cenna.
Valgrind – co jeszcze warto wiedzieć?
W niektórych sytuacjach warto wcześniej sprawdzić program, uruchamiając go w trybie debugowania pod debuggerem, który powinien wskazać znajdujące się w nim błędy. Gdy ta metoda zawiedzie, wtedy w drugiej kolejności powinniśmy skorzystać z Valgrinda. Narzędzie to możemy też wykorzystać do sprawdzenia poprawności gotowego programu. Możemy wówczas uruchomić go pod Valgrindem, sprawdzić każdą ścieżkę i przejrzeć raport z analizy.
Jeśli nie będzie w tym raporcie wycieków ani błędów związanych z pamięcią, możemy powiedzieć, że nasz program jest stabilny i wykonaliśmy dobrą robotę.
Niestety za pomocą Valgrinda nie uda nam się zdebugować kodu, który działa na uC. Możemy jednak postarać się podczas pisania kodu dla mikrokontrolera, aby był on modułowy. Dzięki temu można będzie przetestować jego fragmenty na komputerze.
Podsumowanie
Celem tego wpisu było krótkie przedstawienie przydatnych narzędzi, które warto znać, bo mogą się przydać nawet w amatorskich, hobbystycznych projektach. ClangFormat będzie pomocny nawet dla osób początkujących. Z kolei korzystanie z narzędzi do statycznej i dynamicznej analizy programu wymaga trochę większego doświadczenia, ale warto chociaż pamiętać o takiej możliwości.
Programy opisane w tym artykule mogą być też świetnym narzędziem edukacyjnym. Nie ma nic lepszego od nauki na własnych błędach. Podczas analizy napisanego przez nas kodu zostaniemy poinformowani o złych praktykach i problematycznych miejscach. Dzięki takim informacjom od razu wiadomo, w jakich aspektach warto się jeszcze trochę podszkolić.
Autor: Mateusz Patyk
3 różnice w programowaniu: hobbystycznie vs. komercyjnie
W pewnym momencie każdy programista musi przestawić się z hobbystycznego kodowania na bardziej profesjonalne podejście do tematu. Czym różni... Czytaj dalej »
To nie koniec, sprawdź również
Przeczytaj powiązane artykuły oraz aktualnie popularne wpisy lub losuj inny artykuł »
formatowanie, kod, narzędzia, programowanie
Trwa ładowanie komentarzy...