Blog

C++, Junior, Regular

Moduły w C++

09 Sty 2019

Ostatnie wpisy, tyczące się najlepszego języka programowania jaki kiedykolwiek wymyślił Bjarne Stroustrup, tyczyły się rzeczy na wskroś archaicznych. Dlatego dziś będzie wpis o czymś nowym. O czymś, co dopiero nadciąga. O raku trawiącym C++ i próbach pozbycia się szkodnika.

Tak, proszę Państwa, będzie krótko o modułach.

Wokół systemu dołączania „bibliotek” w C++ narosło już wiele mocno ugruntowanych opinii. Myślę, że moja nie będzie w żaden sposób odbiegająca od normy.
System ten jest po prostu najzwyczajniej w świecie głupi. Ale zarazem śmieszny. Wszak mało gdzie metoda kopiego-pejsta (przynajmniej na poziomie koncepcyjnym) jest aż tak szeroko stosowana.

Problem

Dyrektywa #include to zło wpływa na development w C++ w sposób zdecydowanie nieprzyjemny. To jest fakt.
Każdy, kto chociaż raz brał udział w projekcie większym niż kilka klas doskonale zna następujące problemy:

  • wydłużony czas kompilacji
  • konieczność używania include guard’ów
  • „code fragility”

Załóżmy, że posiadacie w swoim projekcie pewną bibliotekę, dołączaną w wielu miejscach. Niech to będzie X razy.

Więc każdorazowo, X razy, w momencie dołączenia nagłówka tejże biblioteki do roboty zostanie zaprzęgnięty preprocesor. Zostanie również sparsowany sam nagłówek, jak i każdy inny nagłówek w tej bibliotece. Jak i każdy nagłówek w każdym nagłówku dołączanego do biblioteki. To trwa. Trwa bardzo długo.

Co więcej – załóżmy, że do nagłówka owej biblioteki zostanie wprowadzona drobna zmiana. Uruchamiacie kompilację i obserwujcie jak większość projektu właśnie się kompiluje od nowa.
Równie dobrze można udać się do kuchni na kawę/herbatę.

Fakt, istnieją pewnie zabiegi mające na celu zniwelowanie tego problemu – jak na przykład precompiled headers (np. https://github.com/sakra/cotire) niemniej i tak w przypadku zmiany naszej biblioteki projekt zostanie skompilowany ponownie (w pewnym stopniu, oczywiście).

Idźmy dalej – kto choć raz musiał poprawiać kompilacji przez naprawienie kolejności includów? Kto choć raz poprawiał include guarda zawierającego podwójne podkreślenie?

Wszystko to niestety daje dość smutny obraz developmentu w C++ – procesu żmudnego i długiego, wielokrotnie powtarzającego te same czynności. A o tym, jak łatwo popełnić błąd nie muszę chyba nikogo przekonywać.

Rozwiązanie problemu?

Do komitetu standaryzacyjnego C++ zgłoszony został, swego czasu, dokument (P0142R0) mający za zadanie okiełznanie tej nieszczęśliwej sytuacji.

Papier ten, jak i kilka kolejnych powstałych na jego bazie stanowiły zalążek idei modułów w C++. Główne założenia modułów (w bardzo dużym uproszczeniu) to:

  • koniec z rozdzielaniem kodu na header oraz implementację
  • poprawienie czasów kompilacji
  • umożliwienie dołączania bibliotek na zasadzie kompozycji
  • większa granulacja importowanych bibliotek

W jaki sposób to wszystko ma się dokonać? Ano, spójrzmy na trywialny przykład korzystający z modułów (pomysł zaczerpnięty z wystąpienia Nathan Sidwell “C++ Modules” na CppCon2018):

import foo;
import std.core

int main()
{ 
    std::cout << bar(42) << std::endl; //42
    std::cout << bar(1) << std::endl; //43
}

Natomiast nasz przykładowy moduł powinien wyglądać, mniej więcej, następująco:

export module foo;
int acc = 0;
export int bar(int val)
{
    acc += val;
    return val;
}

Warte zauważenia jest:

  • definiowanie modułu związane będzie z konkretną składnią, gdzie powinniśmy zdefiniować, co będzie widoczne dla użytkowników biblioteki
  • szczegóły implementacyjne modułu pozostają niewidoczne dla użytkownika biblioteki
  • importując moduł powinniśmy z założenia korzystać tylko z jego interfejsu

Wymienione powyżej to najpoważniejsze zmiany. Są one raczej proste w zrozumieniu, dlatego pozwolę sobie nie komentować ich szerzej.

Ciekawszy wątek to zmiany niewidoczne dla programisty na pierwszy rzut oka – mianowicie, zgodnie z założeniami powstanie tylko jeden object file foo, każdorazowe dołączenie biblioteki nie będzie skutkować wspomnianym obciążeniem w postaci preprocesora oraz parsowania. Zamiast tego wykorzystywany będzie plik interfejsu (binary interface). To na twórcy biblioteki spoczywać będzie obowiązek dostarczenia tegoż.

Zapowiadany zysk w szybkości kompilacji powinien wynosić około 20%. Co więcej, nie trzeba będzie już zastanawiać się nad tym, jaką unikalną nazwę nadać dla nagłówka (include guard). Nie trzeba będzie rozdzielać implementacji od nagłówka, itp.

Quo vadis?

Zatem wszystko zdaje się być na dobrej drodze, aby programiści C++ poczuli się chociaż trochę jak elitarne grono piszących w Rust? Ale oczywiście, w świecie C++ nic nie może być zbyt oczywiste i proste do wdrożenia!

Z racji, że nadal toczone są dyskusje nad finalnym kształtem modułów w C++, nie jest pewne czy na 100% ten ficzer zjawi się w C++20. Wszelakie informacje płynące z ISO C++ potwierdzają to założenie, aczkolwiek nie brakuje również głosów mówiących, że stanie się to dopiero w C++23.

Dodatkowo, istnieje wiele problemów wynikających z obecnych założeń modułów (np. jak rozwiązać mapowanie modułów, kolejność kompilacji modułów, czy wdrażać jednolite nazewnictwo modułów, format rozszerzeń dla modułów, jak podchodzić do tematu wstecznej kompatybilności, jak ugryźć temat przestrzeni nazw w połączeniu z modułami, w jaki sposób dystrybuować moduły [tcp/ip, lokalne instalacje?], oraz wiele innych), który nie zostały jeszcze zdefiniowane przez komitet standaryzacyjny. Kilka istotnych tematów jest nadal w fazie dyskusji bądź wstępnej implementacji. Dlatego też, na dzień dzisiejszy niestety, nie można być w 100% pewnym jaki kształt przyjmą moduły w C++.

Jednakże, już teraz można przetestować implementacje modułów istniejące w clangu bądź msvc – oba podejścia (jakże by inaczej) rozwiązują zagadnienia niezdefiniowane przez ISO C++ na swój sposób, oba podejścia są wobec siebie różne, nawet na poziomie wykorzystywanej składni, niemniej oba sprowadzają się do skrócenia czasu kompilacji.

Jeżeli chcecie zgłębić temat modułów dla C++ gorąco zachęcam do skorzystania z niniejszych materiałów:

  • A module system for C++ – wspomniany dokument p0142
  • prezentacja Nathan Sidwell z CppCon2018 o modułach (sama prezentacja trwa około 0.5h, reszta to dyskusja) – tak w zasadzie te ~30 minut wystarczy aby zrozumieć, plus minus, podejście C++ do modułów
  • całkiem dobry wątek na reddit o modułach
  • obecna implementacja modułów dla MSVC oraz dla clang
  • trochę krytyki dyskusji nad obecnym stanem rzeczy w modułach w C++ (od Corentina) część1 oraz część2




Stanisław Kubiak

Software Engineer, który zdążył już złapać kilka siwych włosów od debugowania. Psuje kod zawodowo, potem zawodowo musi go naprawiać – najczęściej pracuje z C++. Zajmował się projektami z głębokiego backendu, jak i tymi tkwiącymi stricte we frontendzie. Rekrutacją zajmuje się od kilku lat.

@Stanisław Kubiak
std::vector & std::list – porównanie #2

std::vector & std::list – porównanie

Referencja kontra Wskaźnik

Angielski (4)
C++ (7)
Junior (12)
Python (3)
Regular (6)
ReverseEngineering (1)
Rozmowa (5)
Różne (4)
Senior (1)
Tips&Tricks (8)
angielski c++ junior python regular rozmowa rozmowa kwalifikacyjna