Witajcie! 🙂
W końcu i na mnie przyszła pora na zapoznanie się z feature’mi ze standardu C++20.
Dzisiaj na pierwszy ogień idą moduły z C++20. Spróbujemy sobie napisać prosty kodzik, który wyświetli nam zawartość aktualnego katalogu (taki ls z shella).
Wersja bez modułów
Na początku spójrzmy na kod napisany bez użycia modułów z C++20. Będą to proste klasy Filesystem i Format, plik main.cpp i cmake.
Klasa Filesystem zapewni nam metodę printCurrentDir()
, w której do sformatowania wyjścia użyje metody cleanupOutput()
z klasy Format.
Kod
1 | // Filesystem.hpp |
1 | // Filesystem.cpp |
1 | // Format.hpp |
1 | // Format.cpp |
W pliku main.cpp stworzymy sobie obiekt klasy Tools i wywołamy na nim metodę: printCurrentDir(), która wyświetli nam zawartość aktualnego katalogu.
1 | // main.cpp |
1 | # CmakeLists.txt |
Kompilacja
Teraz skompilujmy nasz kod:
1 | mkdir build && cd build && cmake .. |
A oto output z konsoli:
Wersja z modułami
Kompilacja najnowszej wersji clang’a
Aby zapewnić dobre wsparcie do budowania modułów, zbudujemy sobie najnowszą wersję kompilatora clang. Poniżej przedstawiam komendy, za pomocą których możemy pobrać repozytorium z kodem kompilatora, a następnie go zbudować.
1 | git clone https://github.com/llvm/llvm-project |
Mój output z testów:
Moduły C++20
To teraz opowiedzmy sobie trochę o modułach w C++. To nowość wprowadzona w standardzie C++20.
Wcześniej deklaracje klas, funkcji i zmiennych szły do plików nagłówkowych *.hpp
(ang. header). Ich minusem jest to, że ich zawartość jest po prostu wklejana do jednostek kompilacji (generalnie to pliki *.cpp
), przez co wydłuża się czas samej kompilacji.
Stosując je trzeba także zadbać o to, aby ich zawartość nie była dołączana podwójnie, przez np. inny plik nagłówkowy, który także includuje ten sam plik. Aby temu zapobiec używa się dyrektyw preprocesora #ifndef
, #define
i #endif
.
1 |
|
Moduły natomiast są takimi oddzielnymi jednostkami, które nie są tak zależne między sobą – są pojedynczymi bytami. Jeśli w danymi module nie było zmian, to nie musi on być rekompilowany. Dzięki temu możemy szybciej skompilować nasz projekt.
Ułatwiają także organizację kodu. Gdy moduł jest gotowy, programista musi go tylko zaimportować i może z niego korzystać. Zobaczymy to później na przykładzie.
W przypadku modułów wygląda to troszkę inaczej:
1 | module; // Deklaracja która jest niezbędna do dodania plików nagłówkowych w starym stylu |
Kod
Pora na kod. Implementację klas podzieliłem na dwa oddzielne pliki. Plik z rozszerzeniem *.cppm
to w przypadku kompilator clang pliki interfejsu modułu.
1 | // Format.cppm |
Tu w implementacji metody cleanupOutput() jest mały bonusik z C++20.
Ponieważ z klasy Filesystem dostaję wpisy z formacie: ./filename
, to aby je wypisać tak jak narzędzie ls
, muszę usunąć 2 pierwsze znaki. Aby zmodyfikować każdy wpis używam metody std::ranges::transform()
wprowadzonej w C++20, która na każdym wywołuje metodę substr()
w celu usunięcia znaków: „./”.
1 | // Format-impl.cpp |
Podobnie dla klasy Filesystem.
1 | // Filesystem.cppm |
1 | // Filesystem-impl.cpp |
1 | // main.cpp |
Ostatni element – plik makefile. W przykładzie bez modułów użyłem cmake.
Ale tutaj dla łatwiejszej kompilacji klas, które są rozdzielone na pliki *.cppm
i cpp
, używam zwykłego pliku Makefile, w którym są ręcznie napisane komendy do kompilacji poszczególnych plików.
Wyjaśnijmy sobie dla przykładu kompilację klasy Filesystem.
Najpierw mamy kompilację interfejsu modułu do formatu pośredniego z roszerzeniem *.pcm
:
Filesystem.pcm: Filesystem.cppm
$(CC) $(CFLAGS) Filesystem.cppm --precompile -o Filesystem.pcm
Następnie z pliku pośredniego tworzymy plik obiektowy:
Filesystem.o: Filesystem.pcm
$(CC) $(CFLAGS) Filesystem.pcm -c -o Filesystem.o
I na końcu kompiluję plik z implementacją Filesystem-impl.cpp do pliku obiektowego Filesystem-impl.o. Ponieważ w tym pliku z implementację importujemy moduł Filesystem, to musimy podać plik pośredni, który został wygenerowany z naszej deklaracji modułu w formacie: -fmodule-file=<module-name>=<path/to/*.pcm>
.
W metodzie printCurrentDir() korzystamy też z modułu Format, dlatego musimy także dodać ścieżkę do skompilowanych modułów za pomocą opcji: -fprebuilt-module-path=<path>
.
Filesystem-impl.o: Filesystem-impl.cpp Filesystem.pcm
$(CC) $(CFLAGS) Filesystem-impl.cpp -fmodule-file=Filesystem=Filesystem.pcm -fprebuilt-module-path=. -c -o Filesystem-impl.o
Spójrzmy teraz na całą zawartość pliku Makefile. Na początku zdefiniowałem stałą clang, w której podaję ścieżkę do narzędzia clang++, które wcześniej skompilowaliśmy.
1 | clang="[PATH_TO_COMPILED_LLVM]/llvm-project/build/bin/clang++" |
Kompilacja
make
./Side-ls
A oto output z konsoli:
Podsumowanie
Jak widać na powyższym przykładzie, używanie modułów z C++ nie jest takie straszne 🙂 Najbardziej problematyczne może być pisanie plików makefile. Trzeba tylko zrozumieć, jakie zależności musimy podać w którym miejscu.
Było mi bardzo przyjemnie w końcu zapoznać się z pierwszą rzeczą ze standardu C++20. To pierwszy wpis z tej serii – mam nadzieję, że niedługo powstanie kolejny.
Trzymajcie się!
Link do projektu: https://github.com/Sidewinder22/Side-ls
Bibliografia
- https://clang.llvm.org/docs/StandardCPlusPlusModules.html
- https://github.com/llvm/llvm-project
- https://clang.llvm.org/get_started.html
- https://en.cppreference.com/w/cpp/algorithm/ranges/transform
Pozdrawiam,
{\_Sidewinder22_/}