Ogólnie jestem nowy w programowaniu, więc zdecydowałem, że zacznę od stworzenia prostej klasy wektorowej w C ++. Jednak od samego początku chciałbym wypracować dobre nawyki, zamiast próbować później modyfikować mój przepływ pracy.
Obecnie mam tylko dwa pliki vector3.hpp
i vector3.cpp
. Ten projekt będzie się powoli rozwijał (czyniąc go bardziej ogólną biblioteką algebry liniowej), gdy będę się ze wszystkim lepiej zaznajamiał, więc chciałbym przyjąć „standardowy” układ projektu, aby później ułatwić życie. Więc po rozejrzeniu się znalazłem dwa sposoby organizowania plików hpp i cpp, pierwszy to:
project
└── src
├── vector3.hpp
└── vector3.cpp
a druga istota:
project
├── inc
│ └── project
│ └── vector3.hpp
└── src
└── vector3.cpp
Które byś polecił i dlaczego?
Po drugie, chciałbym użyć Google C ++ Testing Framework do testowania jednostkowego mojego kodu, ponieważ wydaje się on dość łatwy w użyciu. Czy sugerujesz dołączenie tego do mojego kodu, na przykład w folderze inc/gtest
lub contrib/gtest
? Jeśli jest dołączony, czy sugerujesz użycie fuse_gtest_files.py
skryptu w celu zmniejszenia liczby lub plików, czy też pozostawienie go tak, jak jest? Jeśli nie w pakiecie, jak jest obsługiwana ta zależność?
Jeśli chodzi o pisanie testów, jak są one ogólnie zorganizowane? Myślałem o jednym pliku cpp dla każdej klasy ( test_vector3.cpp
na przykład), ale wszystkie skompilowane do jednego pliku binarnego, aby można je było łatwo uruchomić razem?
Ponieważ biblioteka gtest jest generalnie budowana przy użyciu cmake i make, pomyślałem, że miałoby sens, aby mój projekt był również budowany w ten sposób? Gdybym zdecydował się na następujący układ projektu:
├── CMakeLists.txt
├── contrib
│ └── gtest
│ ├── gtest-all.cc
│ └── gtest.h
├── docs
│ └── Doxyfile
├── inc
│ └── project
│ └── vector3.cpp
├── src
│ └── vector3.cpp
└── test
└── test_vector3.cpp
Jak musiałby CMakeLists.txt
wyglądać, aby mógł budować tylko bibliotekę lub bibliotekę i testy? Widziałem też sporo projektów, które mają build
i do bin
katalogu. Czy kompilacja odbywa się w katalogu kompilacji, a następnie pliki binarne są przenoszone do katalogu bin? Czy pliki binarne dla testów i biblioteki będą znajdować się w tym samym miejscu? A może bardziej sensowne byłoby sformułowanie tego w następujący sposób:
test
├── bin
├── build
└── src
└── test_vector3.cpp
Chciałbym również użyć doxygen do udokumentowania mojego kodu. Czy możliwe jest automatyczne uruchamianie tego z cmake i make?
Przepraszam za tak wiele pytań, ale nie znalazłem książki o C ++, która w zadowalający sposób odpowiada na tego typu pytania.
źródło
Odpowiedzi:
Systemy kompilacji C ++ są trochę czarną sztuką i im starszy projekt, tym bardziej dziwne rzeczy można znaleźć, więc nie jest zaskakujące, że pojawia się wiele pytań. Spróbuję przejść przez pytania jeden po drugim i wspomnieć o kilku ogólnych rzeczach dotyczących budowania bibliotek C ++.
Oddzielanie nagłówków i plików cpp w katalogach. Jest to niezbędne tylko wtedy, gdy tworzysz komponent, który ma być używany jako biblioteka, a nie rzeczywista aplikacja. Twoje nagłówki są podstawą interakcji użytkowników z Twoją ofertą i muszą zostać zainstalowane. Oznacza to, że muszą znajdować się w podkatalogu (nikt nie chce, aby wiele nagłówków kończyło się na najwyższym poziomie
/usr/include/
), a Twoje nagłówki muszą być w stanie uwzględnić się w takiej konfiguracji.działa dobrze, ponieważ ścieżki dołączania działają i możesz użyć łatwego globowania dla celów instalacji.
Pakowanie zależności: myślę, że w dużej mierze zależy to od zdolności systemu kompilacji do lokalizowania i konfigurowania zależności oraz od tego, jak zależny jest twój kod od pojedynczej wersji. Zależy to również od tego, jak zdolni są Twoi użytkownicy i jak łatwa jest instalacja zależności na ich platformie. CMake zawiera
find_package
skrypt do Google Test. To znacznie ułatwia sprawę. Pójdę z pakietem tylko wtedy, gdy jest to konieczne i unikam tego w przeciwnym razie.Jak budować: unikaj kompilacji w źródle. CMake ułatwia tworzenie kodu źródłowego i znacznie ułatwia życie.
Przypuszczam, że chcesz również użyć CTest do uruchamiania testów dla swojego systemu (jest również wyposażony we wbudowaną obsługę GTest). Ważną decyzją dotyczącą układu katalogu i organizacji testów będzie: czy otrzymujesz podprojekty? Jeśli tak, potrzebujesz trochę pracy podczas konfigurowania CMakeLists i powinieneś podzielić podprojekty na podkatalogi, każdy z własnymi
include
isrc
plikami. Może nawet ich własne przebiegi i wyjścia doxygen (łączenie wielu projektów doxygen jest możliwe, ale nie jest łatwe ani ładne).Skończysz z czymś takim:
gdzie
Jeśli masz podkomponenty, sugerowałbym dodanie innej hierarchii i użycie powyższego drzewa dla każdego podprojektu. Wtedy sprawy stają się trudne, ponieważ musisz zdecydować, czy podkomponenty przeszukują i konfigurują ich zależności, czy też robisz to na najwyższym poziomie. Decyzja o tym powinna być podejmowana indywidualnie dla każdego przypadku.
Doxygen: Po przejściu przez taniec konfiguracji doxygen, użycie CMake
add_custom_command
do dodania celu dokumentu jest trywialne .Tak kończą się moje projekty i widziałem kilka bardzo podobnych projektów, ale oczywiście to nie jest lekarstwo na wszystko.
Dodatek W pewnym momencie będziesz chciał wygenerować
config.hpp
plik, który zawiera definicję wersji i być może definicję jakiegoś identyfikatora kontroli wersji (skrót Git lub numer wersji SVN). CMake ma moduły do automatyzacji wyszukiwania tych informacji i generowania plików. Możesz użyć CMake,configure_file
aby zastąpić zmienne w pliku szablonu zmiennymi zdefiniowanymi wewnątrzCMakeLists.txt
.Jeśli budujesz biblioteki, będziesz również potrzebować definicji eksportu, aby uzyskać właściwą różnicę między kompilatorami, np.
__declspec
Na MSVC ivisibility
atrybutach na GCC / clang.źródło
Na początek istnieje kilka konwencjonalnych nazw katalogów, których nie można zignorować. Są one oparte na długiej tradycji z systemem plików Unix. To są:
Prawdopodobnie dobrze jest trzymać się tego podstawowego układu, przynajmniej na najwyższym poziomie.
Jeśli chodzi o dzielenie plików nagłówkowych i plików źródłowych (cpp), oba schematy są dość powszechne. Jednak wolę trzymać je razem, po prostu bardziej praktyczne jest trzymanie plików razem w codziennych zadaniach. Ponadto, gdy cały kod znajduje się w jednym folderze najwyższego poziomu, tj.
trunk/src/
Folderze, można zauważyć, że wszystkie inne foldery (bin, lib, include, doc i może jakiś folder testowy) na najwyższym poziomie, oprócz katalog „build” dla kompilacji spoza źródła to wszystkie foldery, które zawierają tylko pliki generowane w procesie budowania. Dlatego tylko folder src musi być zarchiwizowany lub, o wiele lepiej, przechowywany w systemie kontroli wersji / serwerze (takim jak Git lub SVN).A jeśli chodzi o instalowanie plików nagłówkowych w systemie docelowym (jeśli chcesz ostatecznie dystrybuować swoją bibliotekę), cóż, CMake ma polecenie do instalowania plików (niejawnie tworzy cel „instalacji”, aby wykonać „instalację”), które możesz użyć, aby umieścić wszystkie nagłówki w
/usr/include/
katalogu. W tym celu używam tylko następującego makra cmake:Gdzie
SRCROOT
jest zmienna cmake, którą ustawiłem na folder src, iINCLUDEROOT
jest to zmienna cmake, którą konfiguruję, aby wszędzie tam, gdzie mają iść nagłówki. Oczywiście jest na to wiele innych sposobów i jestem pewien, że mój sposób nie jest najlepszy. Chodzi o to, że nie ma powodu, aby dzielić nagłówki i źródła tylko dlatego, że tylko nagłówki muszą być zainstalowane w systemie docelowym, ponieważ jest bardzo łatwe, szczególnie z CMake (lub CPack), aby wybrać i skonfigurować nagłówki do być instalowane bez konieczności umieszczania ich w oddzielnym katalogu. I to właśnie widziałem w większości bibliotek.Nie łącz zależności ze swoją biblioteką. Generalnie jest to dość okropny pomysł i zawsze go nienawidzę, kiedy utknąłem, próbując zbudować bibliotekę, która to zrobiła. To powinna być twoja ostatnia deska ratunku i uważaj na pułapki. Często ludzie łączą zależności ze swoją biblioteką albo dlatego, że celują w okropne środowisko programistyczne (np. Windows), albo dlatego, że obsługują tylko starą (przestarzałą) wersję danej biblioteki (zależność). Główną pułapką jest to, że Twoja powiązana zależność może kolidować z już zainstalowanymi wersjami tej samej biblioteki / aplikacji (np. Spakowałeś gtest, ale osoba próbująca zbudować bibliotekę ma już zainstalowaną nowszą (lub starszą) wersję gtest, a następnie mogą się zderzyć i sprawić tej osobie bardzo okropny ból głowy). Więc, jak powiedziałem, zrób to na własne ryzyko, i powiedziałbym tylko w ostateczności. Poproszenie ludzi o zainstalowanie kilku zależności, zanim będzie można skompilować bibliotekę, jest znacznie mniejszym złem niż próba rozwiązania konfliktów między powiązanymi zależnościami a istniejącymi instalacjami.
Jeden plik cpp na klasę (lub małą spójną grupę klas i funkcji) jest moim zdaniem bardziej zwyczajny i praktyczny. Jednak zdecydowanie nie kompiluj ich wszystkich w jeden plik binarny tylko po to, aby „wszystkie mogły działać razem”. To naprawdę zły pomysł. Ogólnie rzecz biorąc, jeśli chodzi o kodowanie, chcesz podzielić rzeczy na tyle, na ile jest to uzasadnione. W przypadku testów jednostkowych nie chcesz, aby jeden plik binarny uruchamiał wszystkie testy, ponieważ oznacza to, że każda niewielka zmiana, którą wprowadzisz do czegokolwiek w swojej bibliotece, może spowodować prawie całkowitą rekompilację tego programu testów jednostkowych , a to tylko minuty / godziny stracone w oczekiwaniu na rekompilację. Po prostu trzymaj się prostego schematu: 1 jednostka = 1 program testów jednostkowych. Następnie,
Raczej zasugerowałbym taki układ:
Kilka rzeczy, na które należy zwrócić uwagę. Po pierwsze, podkatalogi twojego katalogu src powinny odzwierciedlać podkatalogi twojego katalogu include, to tylko po to, aby zachować intuicyjność (także staraj się utrzymać strukturę podkatalogów w miarę płaską (płytką), ponieważ głębokie zagnieżdżanie folderów jest często bardziej kłopotliwa niż cokolwiek innego). Po drugie, katalog „include” jest po prostu katalogiem instalacyjnym, a jego zawartością są po prostu wszystkie nagłówki, które są pobierane z katalogu src.
Po trzecie, system CMake ma być dystrybuowany w podkatalogach źródłowych, a nie jako jeden plik CMakeLists.txt na najwyższym poziomie. Dzięki temu rzeczy są lokalne i to jest dobre (w duchu dzielenia rzeczy na niezależne części). Jeśli dodasz nowe źródło, nowy nagłówek lub nowy program testowy, wszystko, czego potrzebujesz, to edytować jeden mały i prosty plik CMakeLists.txt w danym podkatalogu, bez wpływu na cokolwiek innego. Pozwala to również na łatwą restrukturyzację katalogów (listy CMakeLists są lokalne i zawarte w przenoszonych podkatalogach). CMakeLists najwyższego poziomu powinny zawierać większość konfiguracji najwyższego poziomu, takich jak konfigurowanie katalogów docelowych, niestandardowe polecenia (lub makra) i znajdowanie pakietów zainstalowanych w systemie. Niższy poziom CMakeLists powinien zawierać tylko proste listy nagłówków, źródeł,
Podstawowa odpowiedź jest taka, że CMake pozwala konkretnie wykluczyć określone cele ze „wszystkich” (co jest budowane po wpisaniu „make”), a także można tworzyć określone zestawy celów. Nie mogę tutaj zrobić samouczka dotyczącego CMake, ale dość łatwo jest się przekonać samemu. Jednak w tym konkretnym przypadku zalecanym rozwiązaniem jest oczywiście użycie CTest, który jest tylko dodatkowym zestawem poleceń, których można użyć w plikach CMakeLists do zarejestrowania wielu celów (programów) oznaczonych jako jednostka- testy. Tak więc CMake umieści wszystkie testy w specjalnej kategorii kompilacji i właśnie o to prosiłeś, więc problem został rozwiązany.
Posiadanie katalogu kompilacji poza źródłem (kompilacja „poza kodem”) jest naprawdę jedyną rozsądną rzeczą do zrobienia, jest to obecnie de facto standard. Tak więc na pewno mam oddzielny katalog "build", poza katalogiem źródłowym, tak jak zalecają ludzie z CMake i jak każdy programista, którego kiedykolwiek spotkałem. Jeśli chodzi o katalog bin to cóż, jest to konwencja i chyba warto się jej trzymać, o czym wspomniałem na początku tego wpisu.
Tak. To więcej niż możliwe, jest niesamowite. W zależności od tego, jak fantazyjny chcesz uzyskać, istnieje kilka możliwości. CMake ma moduł dla Doxygen (tj.
find_package(Doxygen)
), Który pozwala na rejestrowanie celów, które będą uruchamiać Doxygen na niektórych plikach. Jeśli chcesz robić bardziej wymyślne rzeczy, takie jak aktualizowanie numeru wersji w Doxyfile lub automatyczne wprowadzanie znaczników daty / autora dla plików źródłowych i tak dalej, to wszystko jest możliwe dzięki odrobinie kung-fu CMake. Ogólnie rzecz biorąc, zrobienie tego będzie wymagało zachowania źródła Doxyfile (np. „Doxyfile.in”, który umieściłem w układzie folderów powyżej), które zawiera tokeny do znalezienia i zastąpienia przez polecenia parsujące CMake. W moim pliku CMakeLists najwyższego poziomu , znajdziesz jeden taki kawałek kung-fu CMake, który robi kilka wymyślnych rzeczy razem z cmake-doxygen.źródło
main.cpp
powinien iśćtrunk/bin
?Strukturyzacja projektu
Generalnie wolałbym następujące:
Oznacza to, że masz bardzo jasno zdefiniowany zestaw plików API dla swojej biblioteki, a struktura oznacza, że klienci Twojej biblioteki zrobią
zamiast mniej wyraźnych
Podoba mi się struktura drzewa / src, aby była zgodna ze strukturą drzewa / include, ale to naprawdę osobiste preferencje. Jeśli jednak twój projekt rozszerza się, aby zawierać podkatalogi w / include / project, ogólnie byłoby pomocne dopasowanie tych w drzewie / src.
Jeśli chodzi o testy, wolę trzymać je „blisko” plików, które testują, a jeśli skończysz z podkatalogami w / src, jest to całkiem łatwy paradygmat dla innych, jeśli chcą znaleźć kod testowy danego pliku.
Testowanie
Gtest jest rzeczywiście prosty w użyciu i dość wszechstronny pod względem swoich możliwości. Można go bardzo łatwo używać razem z gmockiem, aby rozszerzyć jego możliwości, ale moje własne doświadczenia z gmockiem są mniej korzystne. Jestem całkiem przygotowany, aby zaakceptować, że może to być spowodowane moimi własnymi wadami, ale testy gmocka są zwykle trudniejsze do stworzenia i znacznie bardziej delikatne / trudne do utrzymania. Dużym gwoździem do trumny gmocka jest to, że naprawdę nie gra dobrze z inteligentnymi wskazówkami.
To bardzo banalna i subiektywna odpowiedź na ogromne pytanie (które prawdopodobnie tak naprawdę nie należy do SO)
Wolę używać
ExternalProject_Add
modułu CMake . Pozwala to uniknąć konieczności przechowywania kodu źródłowego gtest w repozytorium lub instalowania go w dowolnym miejscu. Jest automatycznie pobierany i wbudowywany w drzewo kompilacji.Zobacz moją odpowiedź dotyczącą szczegółów tutaj .
Dobry plan.
Budynek
Jestem fanem CMake, ale podobnie jak w przypadku pytań związanych z testami, SO prawdopodobnie nie jest najlepszym miejscem do proszenia o opinie w tak subiektywnej kwestii.
Biblioteka pojawi się jako docelowa „ProjectLibrary”, a zestaw testów jako docelowy „ProjectTest”. Określając bibliotekę jako zależność od testowego exe, zbudowanie testowego exe automatycznie spowoduje odbudowę biblioteki, jeśli jest nieaktualna.
CMake zaleca kompilacje „poza źródłem”, tj. Tworzysz własny katalog kompilacji poza projektem i stamtąd uruchamiasz CMake. Pozwala to uniknąć "zanieczyszczania" twojego drzewa źródłowego plikami kompilacji i jest wysoce pożądane, jeśli używasz vcs.
Państwo może określić, że pliki binarne są przeniesione lub skopiowane do innego katalogu raz zbudowany, lub że są domyślnie tworzone w innym katalogu, ale generalnie nie ma potrzeby. CMake zapewnia wszechstronne sposoby instalowania projektu w razie potrzeby lub ułatwia innym projektom CMake „znajdowanie” odpowiednich plików projektu.
W odniesieniu do własnego wsparcia CMake w znajdowaniu i wykonywaniu testów gtest , byłoby to w dużej mierze niewłaściwe, jeśli tworzysz gtest jako część projektu.
FindGtest
Moduł jest rzeczywiście przeznaczony do stosowania w przypadku, gdy został zbudowany numeru GTEST osobno poza projektem.CMake zapewnia własną strukturę testową (CTest) i idealnie byłoby, gdyby każdy przypadek gtest został dodany jako przypadek CTest.
Jednak
GTEST_ADD_TESTS
makro udostępniane przez,FindGtest
które umożliwia łatwe dodawanie przypadków gtest jako indywidualnych przypadków ctest, jest nieco niedostępne, ponieważ nie działa z makrami gtest innych niżTEST
iTEST_F
. Value- lub Type parametryzacja testy przy użyciuTEST_P
,TYPED_TEST_P
itp nie są obsługiwane w ogóle.Problem nie ma łatwego rozwiązania, które znam. Najbardziej niezawodnym sposobem uzyskania listy przypadków gtest jest wykonanie pliku exe testowego z flagą
--gtest_list_tests
. Jednak można to zrobić tylko po skompilowaniu exe, więc CMake nie może tego wykorzystać. Co pozostawia ci dwie możliwości; CMake musi spróbować przeanalizować kod C ++, aby wydedukować nazwy testów (w skrajnym przypadku nietrywialne, jeśli chcesz wziąć pod uwagę wszystkie makra gtest, zakomentowane testy, wyłączone testy) lub przypadki testowe są dodawane ręcznie do Plik CMakeLists.txt.Tak - chociaż nie mam doświadczenia na tym froncie. CMake zapewnia
FindDoxygen
w tym celu.źródło
Oprócz innych (znakomitych) odpowiedzi, opiszę strukturę, z której korzystałem w stosunkowo dużych projektach.
Nie mam zamiaru zajmować się pytaniem cząstkowym dotyczącym Doxygena, ponieważ chciałbym tylko powtórzyć to, co zostało powiedziane w innych odpowiedziach.
Racjonalne uzasadnienie
Ze względu na modułowość i łatwość konserwacji projekt jest zorganizowany jako zestaw małych jednostek. Dla jasności nazwijmy je UnitX, gdzie X = A, B, C, ... (ale mogą mieć dowolną ogólną nazwę). Struktura katalogów jest następnie organizowana w celu odzwierciedlenia tego wyboru, z możliwością grupowania jednostek, jeśli to konieczne.
Rozwiązanie
Podstawowy układ katalogów jest następujący (zawartość jednostek jest szczegółowo opisana w dalszej części):
project/CMakeLists.txt
może zawierać:i
project/GroupA/CMakeLists.txt
:i
project/GroupB/CMakeLists.txt
:Przejdźmy teraz do struktury różnych jednostek (weźmy na przykład UnitD)
Do różnych komponentów:
.cpp
) i headers (.h
) w tym samym folderze. Pozwala to uniknąć zduplikowanej hierarchii katalogów i ułatwia konserwację. W przypadku instalacji nie ma problemu (zwłaszcza z CMake), aby po prostu filtrować pliki nagłówkowe.UnitD
jest późniejsze zezwolenie na dołączanie plików z rozszerzeniem#include <UnitD/ClassA.h>
. Ponadto podczas instalowania tego urządzenia możesz po prostu skopiować strukturę katalogów w obecnej postaci. Pamiętaj, że możesz również organizować pliki źródłowe w podkatalogach.README
plik podsumowujący, o co chodzi w tym urządzeniu i zawierający przydatne informacje na jego temat.CMakeLists.txt
może po prostu zawierać:lib/CMakeLists.txt
:W tym miejscu należy zauważyć, że nie jest konieczne mówienie CMake, że chcemy, aby katalogi dołączane były dla
UnitA
iUnitC
, ponieważ zostało to już określone podczas konfigurowania tych jednostek. PonadtoPUBLIC
powie wszystkim zależnym obiektom docelowym,UnitD
że powinny automatycznie uwzględniaćUnitA
zależność, podczas gdyUnitC
nie będą wtedy wymagane (PRIVATE
).test/CMakeLists.txt
(zobacz poniżej, jeśli chcesz użyć do tego GTest):Korzystanie z GoogleTest
W przypadku Google Test najłatwiej jest, jeśli jego źródło znajduje się gdzieś w katalogu źródłowym, ale nie musisz go tam dodawać samodzielnie. Używałem tego projektu do automatycznego pobierania i zawijam jego użycie w funkcję, aby upewnić się, że zostanie pobrany tylko raz, mimo że mamy kilka celów testowych.
Ta funkcja CMake jest następująca:
a następnie, gdy chcę go użyć w jednym z moich celów testowych, dodam następujące wiersze do
CMakeLists.txt
(tak jest w powyższym przykładzietest/CMakeLists.txt
):źródło