องค์กรโครงการ C ++ (พร้อม gtest, cmake และ doxygen)


123

ฉันยังใหม่กับการเขียนโปรแกรมโดยทั่วไปดังนั้นฉันจึงตัดสินใจว่าจะเริ่มต้นด้วยการสร้างคลาสเวกเตอร์อย่างง่ายใน C ++ อย่างไรก็ตามฉันต้องการสร้างนิสัยที่ดีตั้งแต่เริ่มต้นแทนที่จะพยายามแก้ไขขั้นตอนการทำงานของฉันในภายหลัง

ขณะนี้ฉันมีเพียงสองไฟล์vector3.hppและvector3.cpp. โครงการนี้จะเริ่มเติบโตอย่างช้าๆ (ทำให้เป็นไลบรารีพีชคณิตเชิงเส้นทั่วไปมากขึ้น) เมื่อฉันคุ้นเคยกับทุกสิ่งมากขึ้นฉันจึงต้องการใช้เค้าโครงโครงการ "มาตรฐาน" เพื่อทำให้ชีวิตง่ายขึ้นในภายหลัง หลังจากมองไปรอบ ๆ ฉันได้พบสองวิธีในการจัดระเบียบไฟล์ hpp และ cpp วิธีแรก:

project
└── src
    ├── vector3.hpp
    └── vector3.cpp

และสิ่งที่สอง:

project
├── inc
   └── project
       └── vector3.hpp
└── src
    └── vector3.cpp

คุณจะแนะนำตัวไหนและเพราะอะไร

ประการที่สองฉันต้องการใช้ Google C ++ Testing Framework สำหรับการทดสอบหน่วยของฉันเนื่องจากดูเหมือนว่าจะใช้งานง่าย คุณแนะนำให้รวมสิ่งนี้เข้ากับรหัสของฉันเช่นในinc/gtestหรือcontrib/gtestโฟลเดอร์หรือไม่ หากรวมกลุ่มคุณแนะนำให้ใช้fuse_gtest_files.pyสคริปต์เพื่อลดจำนวนหรือไฟล์หรือปล่อยให้เป็น หากไม่ได้รวมการพึ่งพานี้จะจัดการอย่างไร

เมื่อพูดถึงการทดสอบการเขียนโดยทั่วไปมีการจัดระเบียบอย่างไร? ฉันคิดว่าจะมีไฟล์ cpp หนึ่งไฟล์สำหรับแต่ละคลาส ( test_vector3.cppเช่น) แต่ทั้งหมดถูกคอมไพล์เป็นไบนารีเดียวเพื่อให้สามารถรันร่วมกันได้อย่างง่ายดาย?

เนื่องจากไลบรารี gtest มักสร้างโดยใช้ cmake และ make ฉันจึงคิดว่ามันจะสมเหตุสมผลไหมที่โครงการของฉันจะสร้างแบบนี้ หากฉันตัดสินใจใช้เค้าโครงโครงการต่อไปนี้:

├── CMakeLists.txt
├── contrib
   └── gtest
       ├── gtest-all.cc
       └── gtest.h
├── docs
   └── Doxyfile
├── inc
   └── project
       └── vector3.cpp
├── src
   └── vector3.cpp
└── test
    └── test_vector3.cpp

จะCMakeLists.txtต้องมีลักษณะอย่างไรจึงจะสามารถสร้างเฉพาะไลบรารีหรือไลบรารีและการทดสอบได้? นอกจากนี้ฉันยังเห็นโครงการไม่กี่โครงการที่มีbuildและbinไดเรกทอรี บิวด์เกิดขึ้นในไดเร็กทอรีบิลด์จากนั้นไบนารีย้ายออกไปยังไดเร็กทอรี bin หรือไม่ ไบนารีสำหรับการทดสอบและห้องสมุดจะอยู่ในที่เดียวกันหรือไม่ หรือจะเหมาะสมกว่าที่จะจัดโครงสร้างดังต่อไปนี้:

test
├── bin
├── build
└── src
    └── test_vector3.cpp

ฉันต้องการใช้ doxygen เพื่อบันทึกรหัสของฉันด้วย เป็นไปได้หรือไม่ที่จะทำให้สิ่งนี้ทำงานโดยอัตโนมัติด้วย cmake และ make?

ขออภัยสำหรับคำถามมากมาย แต่ฉันไม่พบหนังสือเกี่ยวกับ C ++ ที่ตอบคำถามประเภทนี้ได้อย่างน่าพอใจ


6
คำถามที่ดี แต่ฉันไม่คิดว่าจะเหมาะกับรูปแบบ Q&A ของStack Overflow ฉันสนใจคำตอบมาก +1 & fav
Luchian Grigore

1
เหล่านี้เป็นคำถามมากมายในเรื่องใหญ่ ขอให้แยกเป็นคำถามย่อย ๆ หลาย ๆ คำถามแล้ววางลิงก์ถึงกันจะดีกว่า อย่างไรก็ตามเพื่อตอบส่วนสุดท้าย: ด้วย CMake คุณสามารถเลือกที่จะสร้างภายในและภายนอกไดเร็กทอรี src ของคุณ (ฉันขอแนะนำภายนอก) และใช่คุณสามารถใช้ doxygen กับ CMake โดยอัตโนมัติ
mistapink

คำตอบ:


84

ระบบสร้าง C ++ เป็นศิลปะสีดำเล็กน้อยและยิ่งโครงการมีอายุมากขึ้นคุณก็จะพบสิ่งแปลก ๆ มากขึ้นดังนั้นจึงไม่น่าแปลกใจที่มีคำถามมากมายเกิดขึ้น ฉันจะพยายามตอบคำถามทีละคำถามและพูดถึงสิ่งทั่วไปเกี่ยวกับการสร้างไลบรารี C ++

การแยกส่วนหัวและไฟล์ cpp ในไดเร็กทอรี นี่เป็นสิ่งสำคัญเฉพาะในกรณีที่คุณกำลังสร้างส่วนประกอบที่ควรจะใช้เป็นไลบรารีซึ่งตรงข้ามกับแอปพลิเคชันจริง ส่วนหัวของคุณเป็นพื้นฐานสำหรับผู้ใช้ในการโต้ตอบกับสิ่งที่คุณนำเสนอและต้องติดตั้ง ซึ่งหมายความว่าต้องอยู่ในไดเรกทอรีย่อย (ไม่มีใครต้องการส่วนหัวจำนวนมากที่ลงท้ายด้วยระดับบนสุด/usr/include/) และส่วนหัวของคุณต้องสามารถรวมตัวเองเข้ากับการตั้งค่าดังกล่าวได้

└── prj
    ├── include
       └── prj
           ├── header2.h
           └── header.h
    └── src
        └── x.cpp

ใช้งานได้ดีเพราะรวมเส้นทางไว้แล้วและคุณสามารถใช้ globbing ง่ายสำหรับเป้าหมายการติดตั้ง

การอ้างอิงการรวมกลุ่ม: ฉันคิดว่าสิ่งนี้ส่วนใหญ่ขึ้นอยู่กับความสามารถของระบบสร้างในการค้นหาและกำหนดค่าการอ้างอิงและวิธีการขึ้นอยู่กับรหัสของคุณในเวอร์ชันเดียว นอกจากนี้ยังขึ้นอยู่กับว่าผู้ใช้ของคุณมีความสามารถเพียงใดและการพึ่งพาในการติดตั้งบนแพลตฟอร์มของตนนั้นง่ายเพียงใด CMake มาพร้อมกับfind_packageสคริปต์สำหรับ Google Test สิ่งนี้ทำให้ง่ายขึ้นมาก ฉันจะไปกับการรวมกลุ่มเมื่อจำเป็นเท่านั้นและหลีกเลี่ยงเป็นอย่างอื่น

วิธีการสร้าง: หลีกเลี่ยงการสร้างในแหล่งที่มา CMake ทำให้การสร้างแหล่งที่มาเป็นเรื่องง่ายและทำให้ชีวิตง่ายขึ้นมาก

ฉันคิดว่าคุณต้องการใช้ CTest เพื่อเรียกใช้การทดสอบสำหรับระบบของคุณด้วย (มันมาพร้อมกับการรองรับ GTest ในตัวด้วย) การตัดสินใจที่สำคัญสำหรับเค้าโครงไดเร็กทอรีและองค์กรทดสอบคือ: คุณจบลงด้วยโครงการย่อยหรือไม่? หากเป็นเช่นนั้นคุณต้องทำงานเพิ่มขึ้นเมื่อตั้งค่า CMakeLists และควรแยกโครงการย่อยของคุณออกเป็นไดเรกทอรีย่อยโดยแต่ละโครงการจะมีไฟล์includeและsrcไฟล์ของตัวเอง บางทีแม้แต่การทำงานและเอาต์พุต doxygen ของตัวเอง (การรวมโครงการ doxygen หลายโครงการเป็นไปได้ แต่ไม่ใช่เรื่องง่ายหรือสวย)

คุณจะจบลงด้วยสิ่งนี้:

└── prj
    ├── CMakeLists.txt <-- (1)
    ├── include
       └── prj
           ├── header2.hpp
           └── header.hpp
    ├── src
       ├── CMakeLists.txt <-- (2)
       └── x.cpp
    └── test
        ├── CMakeLists.txt <-- (3)
        ├── data
           └── testdata.yyy
        └── testcase.cpp

ที่ไหน

  • (1) กำหนดค่าการอ้างอิงข้อมูลจำเพาะของแพลตฟอร์มและเส้นทางเอาต์พุต
  • (2) กำหนดค่าไลบรารีที่คุณกำลังจะสร้าง
  • (3) กำหนดค่าไฟล์ปฏิบัติการทดสอบและกรณีทดสอบ

ในกรณีที่คุณมีส่วนประกอบย่อยฉันขอแนะนำให้เพิ่มลำดับชั้นอื่นและใช้โครงสร้างด้านบนสำหรับแต่ละโครงการย่อย จากนั้นสิ่งต่าง ๆ จะยุ่งยากเพราะคุณต้องตัดสินใจว่าส่วนประกอบย่อยค้นหาและกำหนดค่าการอ้างอิงหรือถ้าคุณทำในระดับบนสุด สิ่งนี้ควรตัดสินใจเป็นกรณี ๆ ไป

Doxygen: หลังจากที่คุณสามารถผ่านการเต้นของการกำหนดค่าของ doxygen ได้แล้วการใช้ CMake add_custom_commandเพื่อเพิ่มเป้าหมายเอกสารนั้นเป็นเรื่องเล็กน้อย

นี่เป็นวิธีที่โครงการของฉันจบลงและฉันได้เห็นโครงการที่คล้ายกันมาก แต่แน่นอนว่านี่ไม่ใช่วิธีการรักษาทั้งหมด

ภาคผนวกในบางจุดคุณอาจต้องการสร้างconfig.hpp ไฟล์ที่มีการกำหนดเวอร์ชันและอาจกำหนดให้กับตัวระบุการควบคุมเวอร์ชัน (แฮช Git หรือหมายเลขแก้ไข SVN) CMake มีโมดูลสำหรับค้นหาข้อมูลนั้นและสร้างไฟล์โดยอัตโนมัติ คุณสามารถใช้ CMake configure_fileเพื่อแทนที่ตัวแปรในไฟล์เทมเพลตด้วยตัวแปรที่กำหนดไว้ภายในไฟล์CMakeLists.txt.

หากคุณกำลังสร้างไลบรารีคุณจะต้องกำหนดการส่งออกเพื่อให้ได้ความแตกต่างระหว่างคอมไพเลอร์ที่ถูกต้องเช่น__declspecMSVC และvisibilityแอตทริบิวต์บน GCC / clang


2
คำตอบที่ดี แต่ฉันยังไม่ชัดเจนว่าทำไมคุณต้องใส่ไฟล์ส่วนหัวของคุณในไดเรกทอรีย่อยที่มีชื่อโครงการเพิ่มเติม: "/prj/include/prj/foo.hpp" ซึ่งดูเหมือนจะซ้ำซ้อนสำหรับฉัน ทำไมไม่แค่ "/prj/include/foo.hpp" ล่ะ ฉันสมมติว่าคุณจะมีโอกาสที่จะจิ๊กไดเร็กทอรีการติดตั้งอีกครั้งในเวลาติดตั้งดังนั้นคุณจะได้รับ <INSTALL_DIR> /include/prj/foo.hpp เมื่อคุณติดตั้งหรือ CMake นั้นยากหรือไม่?
William Payne

6
@ วิลเลียมนั่นเป็นเรื่องยากที่จะทำกับ CPack นอกจากนี้ไฟล์แหล่งที่มาของคุณจะมีลักษณะอย่างไร หากเป็นเพียง "header.hpp" ในเวอร์ชันที่ติดตั้ง "/ usr / include / prj /" จะต้องอยู่ในเส้นทาง include ซึ่งตรงข้ามกับ "/ usr / include" เท่านั้น
พีเอ็ม

37

ในฐานะผู้เริ่มต้นมีชื่อทั่วไปสำหรับไดเร็กทอรีที่คุณไม่สามารถเพิกเฉยได้ซึ่งเป็นไปตามประเพณีอันยาวนานของระบบไฟล์ Unix เหล่านี้คือ:

trunk
├── bin     : for all executables (applications)
├── lib     : for all other binaries (static and shared libraries (.so or .dll))
├── include : for all header files
├── src     : for source files
└── doc     : for documentation

อาจเป็นความคิดที่ดีที่จะยึดติดกับเค้าโครงพื้นฐานนี้อย่างน้อยก็ในระดับบนสุด

เกี่ยวกับการแยกไฟล์ส่วนหัวและไฟล์ต้นฉบับ (cpp) ทั้งสองรูปแบบเป็นเรื่องธรรมดา อย่างไรก็ตามฉันมักจะชอบเก็บไว้ด้วยกันมันเป็นประโยชน์มากกว่าสำหรับงานประจำวันที่จะมีไฟล์ร่วมกัน นอกจากนี้เมื่อโค้ดทั้งหมดอยู่ภายใต้โฟลเดอร์ระดับบนสุดเช่นtrunk/src/โฟลเดอร์คุณจะสังเกตได้ว่าโฟลเดอร์อื่น ๆ ทั้งหมด (bin, lib, include, doc และอาจจะเป็นโฟลเดอร์ทดสอบบางโฟลเดอร์) ที่ระดับบนสุดนอกเหนือจาก ไดเร็กทอรี "build" สำหรับบิลด์นอกซอร์สคือโฟลเดอร์ทั้งหมดที่ไม่มีอะไรมากไปกว่าไฟล์ที่สร้างขึ้นในกระบวนการสร้าง ดังนั้นจำเป็นต้องสำรองเฉพาะโฟลเดอร์ src หรือดีกว่ามากโดยเก็บไว้ภายใต้ระบบ / เซิร์ฟเวอร์ควบคุมเวอร์ชัน (เช่น Git หรือ SVN)

และเมื่อพูดถึงการติดตั้งไฟล์ส่วนหัวของคุณบนระบบปลายทาง (หากคุณต้องการกระจายไลบรารีของคุณในที่สุด) CMake มีคำสั่งสำหรับการติดตั้งไฟล์ (โดยปริยายจะสร้างเป้าหมาย "ติดตั้ง" เพื่อทำการ "ทำการติดตั้ง") ซึ่ง คุณสามารถใช้เพื่อใส่ส่วนหัวทั้งหมดลงใน/usr/include/ไดเร็กทอรี ฉันใช้มาโคร cmake ต่อไปนี้เพื่อจุดประสงค์นี้:

# custom macro to register some headers as target for installation:
#  setup_headers("/path/to/header/something.h" "/relative/install/path")
macro(setup_headers HEADER_FILES HEADER_PATH)
  foreach(CURRENT_HEADER_FILE ${HEADER_FILES})
    install(FILES "${SRCROOT}${CURRENT_HEADER_FILE}" DESTINATION "${INCLUDEROOT}${HEADER_PATH}")
  endforeach(CURRENT_HEADER_FILE)
endmacro(setup_headers)

ในกรณีที่SRCROOTเป็นตัวแปร CMake ที่ผมตั้งไปยังโฟลเดอร์ src และINCLUDEROOTเป็นตัวแปร CMake ที่ผมกำหนดค่าไปยังปลายทางที่ส่วนหัวจะต้องไป แน่นอนว่ามีวิธีอื่นอีกมากมายในการทำเช่นนี้และฉันแน่ใจว่าวิธีของฉันไม่ใช่วิธีที่ดีที่สุด ประเด็นคือไม่มีเหตุผลที่จะแยกส่วนหัวและแหล่งที่มาเพียงเพราะจำเป็นต้องติดตั้งเฉพาะส่วนหัวในระบบเป้าหมายเนื่องจากเป็นเรื่องง่ายมากโดยเฉพาะอย่างยิ่งกับ CMake (หรือ CPack) เพื่อเลือกและกำหนดค่าส่วนหัวเป็น ติดตั้งโดยไม่ต้องมีในไดเร็กทอรีแยกต่างหาก และนี่คือสิ่งที่ฉันเห็นในห้องสมุดส่วนใหญ่

ข้อความอ้างอิง: ประการที่สองฉันต้องการใช้ Google C ++ Testing Framework สำหรับการทดสอบหน่วยของฉันเนื่องจากดูเหมือนว่าใช้งานง่าย คุณแนะนำให้รวมสิ่งนี้เข้ากับรหัสของฉันเช่นในโฟลเดอร์ "inc / gtest" หรือ "Contrib / gtest" หรือไม่ หากรวมกลุ่มคุณแนะนำให้ใช้สคริปต์ fuse_gtest_files.py เพื่อลดจำนวนหรือไฟล์หรือปล่อยทิ้งไว้ตามที่เป็นอยู่ หากไม่ได้รวมการพึ่งพานี้จะจัดการอย่างไร

อย่ารวมการอ้างอิงกับไลบรารีของคุณ โดยทั่วไปแล้วนี่เป็นความคิดที่ค่อนข้างน่ากลัวและฉันมักจะเกลียดมันเมื่อฉันพยายามสร้างห้องสมุดที่ทำแบบนั้น ควรเป็นทางเลือกสุดท้ายของคุณและระวังข้อผิดพลาด บ่อยครั้งที่ผู้คนรวมกลุ่มการอ้างอิงกับไลบรารีของพวกเขาไม่ว่าจะเป็นเพราะพวกเขากำหนดเป้าหมายสภาพแวดล้อมการพัฒนาที่แย่มาก (เช่น Windows) หรือเพราะพวกเขาสนับสนุนเฉพาะไลบรารีเวอร์ชันเก่า (เลิกใช้แล้ว) (การพึ่งพา) ที่เป็นปัญหา ข้อผิดพลาดหลักคือการพึ่งพาบันเดิลของคุณอาจปะทะกับไลบรารี / แอปพลิเคชันเดียวกันที่ติดตั้งไว้แล้ว (เช่นคุณรวม gtest แต่ผู้ที่พยายามสร้างไลบรารีของคุณมี gtest เวอร์ชันใหม่กว่า (หรือเก่ากว่า) ที่ติดตั้งไว้แล้ว ทั้งสองอาจปะทะกันและทำให้คนนั้นปวดหัวมาก) อย่างที่บอกไปว่าคุณต้องยอมรับความเสี่ยงเอง และฉันจะพูดเป็นทางเลือกสุดท้ายเท่านั้น การขอให้ผู้คนติดตั้งการอ้างอิงบางส่วนก่อนที่จะสามารถคอมไพล์ไลบรารีของคุณเป็นสิ่งที่ชั่วร้ายน้อยกว่าการพยายามแก้ไขข้อขัดแย้งระหว่างการอ้างอิงที่รวมและการติดตั้งที่มีอยู่

ข้อความอ้างอิง: เมื่อพูดถึงการทดสอบการเขียนโดยทั่วไปมีการจัดระเบียบอย่างไร? ฉันคิดว่าจะมีไฟล์ cpp หนึ่งไฟล์สำหรับแต่ละคลาส (เช่น test_vector3.cpp) แต่คอมไพล์ทั้งหมดเป็นไบนารีเดียวเพื่อให้สามารถรันร่วมกันได้อย่างง่ายดาย?

ไฟล์ cpp หนึ่งไฟล์ต่อคลาส (หรือกลุ่มคลาสและฟังก์ชั่นที่เหนียวแน่นขนาดเล็ก) เป็นเรื่องปกติและใช้งานได้จริงในความคิดของฉัน อย่างไรก็ตามอย่ารวบรวมทั้งหมดเป็นไบนารีเดียวเพียงเพื่อให้ "ทุกคนสามารถทำงานร่วมกันได้" นั่นเป็นความคิดที่แย่จริงๆ โดยทั่วไปแล้วเมื่อพูดถึงการเข้ารหัสคุณต้องแยกสิ่งต่างๆออกมากที่สุดเท่าที่จะทำได้ ในกรณีของการทดสอบหน่วยคุณไม่ต้องการให้ไบนารีหนึ่งรันการทดสอบทั้งหมดเพราะนั่นหมายความว่าการเปลี่ยนแปลงเพียงเล็กน้อยที่คุณทำกับสิ่งใด ๆ ในไลบรารีของคุณมีแนวโน้มที่จะทำให้เกิดการคอมไพล์ใหม่ทั้งหมดของโปรแกรมทดสอบหน่วยนั้น และนั่นเป็นเพียงนาที / ชั่วโมงที่เสียไปกับการรอการคอมไพล์ใหม่ เพียงแค่ยึดติดกับรูปแบบง่ายๆ: 1 หน่วย = 1 โปรแกรมทดสอบหน่วย จากนั้น

ข้อความอ้างอิง: เนื่องจากไลบรารี gtest มักสร้างโดยใช้ cmake และ make ฉันจึงคิดว่ามันจะสมเหตุสมผลไหมที่โครงการของฉันจะสร้างแบบนี้ หากฉันตัดสินใจใช้เค้าโครงโครงการต่อไปนี้:

ฉันอยากจะแนะนำเค้าโครงนี้:

trunk
├── bin
├── lib
   └── project
       └── libvector3.so
       └── libvector3.a        products of installation / building
├── docs
   └── Doxyfile
├── include
   └── project
       └── vector3.hpp
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

├── src
   └── CMakeLists.txt
   └── Doxyfile.in
   └── project                 part of version-control / source-distribution
       └── CMakeLists.txt
       └── vector3.hpp
       └── vector3.cpp
       └── test
           └── test_vector3.cpp
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

├── build
└── test                        working directories for building / testing
    └── test_vector3

ข้อสังเกตบางประการที่นี่ ขั้นแรกไดเร็กทอรีย่อยของไดเร็กทอรี src ของคุณควรสะท้อนไดเร็กทอรีย่อยของไดเร็กทอรี include ของคุณซึ่งเป็นเพียงเพื่อให้สิ่งต่าง ๆ ใช้งานง่าย (นอกจากนี้พยายามทำให้โครงสร้างไดเร็กทอรีย่อยของคุณแบนอย่างสมเหตุสมผล (ตื้น) เนื่องจากการซ้อนโฟลเดอร์อยู่ลึก ๆ มักจะยุ่งยากมากกว่าสิ่งอื่นใด) ประการที่สองไดเร็กทอรี "include" เป็นเพียงไดเร็กทอรีการติดตั้งเนื้อหาเป็นเพียงส่วนหัวที่เลือกออกจากไดเร็กทอรี src

ประการที่สามระบบ CMake มีวัตถุประสงค์เพื่อเผยแพร่ผ่านไดเรกทอรีย่อยต้นทางไม่ใช่ไฟล์ CMakeLists.txt ไฟล์เดียวที่ระดับบนสุด สิ่งนี้ช่วยรักษาสิ่งต่างๆในท้องถิ่นและนั่นเป็นสิ่งที่ดี (ในจิตวิญญาณของการแยกสิ่งต่างๆออกเป็นชิ้นส่วนที่เป็นอิสระ) หากคุณเพิ่มซอร์สใหม่ส่วนหัวใหม่หรือโปรแกรมทดสอบใหม่สิ่งที่คุณต้องมีคือแก้ไขไฟล์ CMakeLists.txt ขนาดเล็กและเรียบง่ายหนึ่งไฟล์ในไดเร็กทอรีย่อยที่เป็นปัญหาโดยไม่ส่งผลกระทบต่อสิ่งอื่นใด นอกจากนี้ยังช่วยให้คุณปรับโครงสร้างไดเรกทอรีได้อย่างง่ายดาย (CMakeLists เป็นแบบโลคัลและมีอยู่ในไดเรกทอรีย่อยที่ถูกย้าย) CMakeLists ระดับบนสุดควรมีการกำหนดค่าระดับบนสุดส่วนใหญ่เช่นการตั้งค่าไดเรกทอรีปลายทางคำสั่งที่กำหนดเอง (หรือมาโคร) และการค้นหาแพ็คเกจที่ติดตั้งบนระบบ CMakeLists ระดับล่างควรมีเฉพาะรายการส่วนหัวแหล่งที่มา

ข้อความอ้างอิง: CMakeLists.txt จะต้องมีลักษณะอย่างไรจึงจะสามารถสร้างเฉพาะไลบรารีหรือไลบรารีและการทดสอบได้

คำตอบพื้นฐานคือ CMake ช่วยให้คุณสามารถยกเว้นเป้าหมายบางเป้าหมายจาก "ทั้งหมด" โดยเฉพาะ (ซึ่งเป็นสิ่งที่สร้างขึ้นเมื่อคุณพิมพ์ "make") และคุณยังสามารถสร้างกลุ่มเป้าหมายเฉพาะได้อีกด้วย ฉันไม่สามารถทำแบบฝึกหัด CMake ได้ที่นี่ แต่มันค่อนข้างตรงไปตรงมาที่จะค้นหาด้วยตัวเอง อย่างไรก็ตามในกรณีนี้วิธีแก้ปัญหาที่แนะนำคือการใช้ CTest ซึ่งเป็นเพียงชุดคำสั่งเพิ่มเติมที่คุณสามารถใช้ในไฟล์ CMakeLists เพื่อลงทะเบียนเป้าหมาย (โปรแกรม) จำนวนหนึ่งที่ทำเครื่องหมายเป็นหน่วย การทดสอบ ดังนั้น CMake จะทำการทดสอบทั้งหมดในหมวดหมู่พิเศษของงานสร้างและนั่นคือสิ่งที่คุณขอเพื่อแก้ไขปัญหา

ข้อความอ้างอิง: นอกจากนี้ฉันยังเห็นโครงการไม่กี่โครงการที่มีไดเร็กทอรี bin โฆษณา build บิวด์เกิดขึ้นในไดเร็กทอรีบิลด์จากนั้นไบนารีย้ายออกไปยังไดเร็กทอรี bin หรือไม่ ไบนารีสำหรับการทดสอบและห้องสมุดจะอยู่ในที่เดียวกันหรือไม่ หรือจะเหมาะสมกว่าที่จะจัดโครงสร้างดังต่อไปนี้:

การมีบิวด์ไดเร็กทอรีนอกซอร์ส (บิวด์ "นอกแหล่ง") เป็นสิ่งเดียวที่ต้องทำจริงๆมันเป็นมาตรฐานโดยพฤตินัยในทุกวันนี้ ดังนั้นแน่นอนว่ามีไดเร็กทอรี "build" แยกต่างหากนอกไดเร็กทอรีซอร์สเช่นเดียวกับที่คน CMake แนะนำและอย่างที่โปรแกรมเมอร์ทุกคนที่ฉันเคยพบเจอ สำหรับไดเร็กทอรี bin นั่นเป็นข้อตกลงและมันเป็นความคิดที่ดีที่จะยึดตามที่ฉันได้กล่าวไว้ในตอนต้นของโพสต์นี้

ข้อความอ้างอิง: ฉันต้องการใช้ doxygen เพื่อบันทึกรหัสของฉันด้วย เป็นไปได้หรือไม่ที่จะทำให้สิ่งนี้ทำงานโดยอัตโนมัติด้วย cmake และ make?

ใช่. เป็นไปได้มากกว่านั้นมันยอดเยี่ยมมาก ขึ้นอยู่กับว่าคุณต้องการจินตนาการอย่างไรมีความเป็นไปได้หลายประการ CMake มีโมดูลสำหรับ Doxygen (เช่นfind_package(Doxygen)) ซึ่งช่วยให้คุณสามารถลงทะเบียนเป้าหมายที่จะเรียกใช้ Doxygen ในไฟล์บางไฟล์ หากคุณต้องการทำสิ่งที่น่าสนใจมากขึ้นเช่นการอัปเดตหมายเลขเวอร์ชันใน Doxyfile หรือการป้อนวันที่ / ตราประทับของผู้แต่งสำหรับไฟล์ต้นฉบับและอื่น ๆ ทั้งหมดนี้เป็นไปได้ด้วย CMake kung-fu โดยทั่วไปการทำเช่นนี้จะเกี่ยวข้องกับการที่คุณเก็บ Doxyfile ต้นทาง (เช่น "Doxyfile.in" ที่ฉันใส่ไว้ในเค้าโครงโฟลเดอร์ด้านบน) ซึ่งมีโทเค็นที่จะพบและแทนที่ด้วยคำสั่งแยกวิเคราะห์ของ CMake ในไฟล์ CMakeLists ระดับบนสุดของฉันคุณจะพบ CMake kung-fu ชิ้นหนึ่งที่ทำสิ่งแปลก ๆ บางอย่างกับ cmake-doxygen ร่วมกัน


จึงmain.cppควรไปที่trunk/bin?
Ugnius Malūkas

17

การจัดโครงสร้างโครงการ

โดยทั่วไปฉันจะชอบสิ่งต่อไปนี้:

├── CMakeLists.txt
|
├── docs/
│   └── Doxyfile
|
├── include/
│   └── project/
│       └── vector3.hpp
|
├── src/
    └── project/
        └── vector3.cpp
        └── test/
            └── test_vector3.cpp

ซึ่งหมายความว่าคุณมีชุดไฟล์ API ที่กำหนดไว้อย่างชัดเจนสำหรับไลบรารีของคุณและโครงสร้างหมายความว่าไคลเอ็นต์ของไลบรารีของคุณจะทำ

#include "project/vector3.hpp"

มากกว่าความชัดเจนน้อยกว่า

#include "vector3.hpp"


ฉันชอบโครงสร้างของต้นไม้ / src เพื่อให้ตรงกับต้นไม้ / รวม แต่นั่นเป็นความชอบส่วนบุคคลจริงๆ อย่างไรก็ตามหากโปรเจ็กต์ของคุณขยายให้มีไดเร็กทอรีย่อยภายใน / รวม / โปรเจ็กต์โดยทั่วไปจะช่วยในการจับคู่ที่อยู่ภายในโครงสร้าง / src

สำหรับการทดสอบฉันชอบให้พวกเขา "ใกล้" กับไฟล์ที่พวกเขาทดสอบและถ้าคุณจบลงด้วยไดเร็กทอรีย่อยภายใน / src มันเป็นกระบวนทัศน์ที่ค่อนข้างง่ายสำหรับผู้อื่นในการทำตามหากพวกเขาต้องการค้นหารหัสทดสอบของไฟล์ที่ระบุ


การทดสอบ

ประการที่สองฉันต้องการใช้ Google C ++ Testing Framework สำหรับการทดสอบหน่วยของฉันเนื่องจากดูเหมือนว่าจะใช้งานง่าย

Gtest นั้นใช้งานง่ายและค่อนข้างครอบคลุมในแง่ของความสามารถ สามารถใช้ร่วมกับgmockได้อย่างง่ายดายเพื่อเพิ่มขีดความสามารถ แต่ประสบการณ์ของฉันกับ gmock นั้นไม่ค่อยดีนัก ฉันค่อนข้างเตรียมพร้อมที่จะยอมรับว่านี่อาจเป็นข้อบกพร่องของตัวเอง แต่การทดสอบ gmock มีแนวโน้มที่จะสร้างได้ยากกว่าและเปราะบาง / รักษายากกว่ามาก ตะปูขนาดใหญ่ในโลงศพของ gmock คือมันเล่นไม่ได้ดีกับตัวชี้อัจฉริยะ

นี่เป็นคำตอบที่ไม่สำคัญและเป็นส่วนตัวสำหรับคำถามใหญ่ ๆ (ซึ่งอาจไม่ได้อยู่ใน SO)

คุณแนะนำให้รวมสิ่งนี้เข้ากับรหัสของฉันเช่นในโฟลเดอร์ "inc / gtest" หรือ "Contrib / gtest" หรือไม่ หากรวมกลุ่มคุณแนะนำให้ใช้สคริปต์ fuse_gtest_files.py เพื่อลดจำนวนหรือไฟล์หรือปล่อยทิ้งไว้ตามที่เป็นอยู่ หากไม่ได้รวมการพึ่งพานี้จะจัดการอย่างไร

ฉันชอบใช้ExternalProject_Addโมดูลของ CMake มากกว่า วิธีนี้ช่วยหลีกเลี่ยงไม่ให้คุณต้องเก็บซอร์สโค้ด gtest ไว้ในที่เก็บของคุณหรือติดตั้งที่ใดก็ได้ มันถูกดาวน์โหลดและสร้างขึ้นในโครงสร้างการสร้างของคุณโดยอัตโนมัติ

ดูของฉันคำตอบการจัดการกับรายละเอียดที่นี่

เมื่อพูดถึงการทดสอบการเขียนโดยทั่วไปมีการจัดระเบียบอย่างไร? ฉันคิดว่าจะมีไฟล์ cpp หนึ่งไฟล์สำหรับแต่ละคลาส (เช่น test_vector3.cpp) แต่คอมไพล์ทั้งหมดเป็นไบนารีเดียวเพื่อให้สามารถรันร่วมกันได้อย่างง่ายดาย?

แผนการที่ดี.


อาคาร

ฉันเป็นแฟนของ CMake แต่เช่นเดียวกับคำถามเกี่ยวกับการทดสอบของคุณ SO อาจไม่ใช่สถานที่ที่ดีที่สุดในการขอความคิดเห็นเกี่ยวกับปัญหาส่วนตัวดังกล่าว

CMakeLists.txt จะต้องมีลักษณะอย่างไรจึงจะสามารถสร้างเฉพาะไลบรารีหรือไลบรารีและการทดสอบได้

add_library(ProjectLibrary <All library sources and headers>)
add_executable(ProjectTest <All test files>)
target_link_libraries(ProjectTest ProjectLibrary)

ไลบรารีจะปรากฏเป็นเป้าหมาย "ProjectLibrary" และชุดทดสอบเป็นเป้าหมาย "ProjectTest" ด้วยการระบุไลบรารีเป็นการอ้างอิงของ exe การทดสอบการสร้าง exe ทดสอบจะทำให้ไลบรารีถูกสร้างใหม่โดยอัตโนมัติหากล้าสมัย

นอกจากนี้ฉันยังได้เห็นโครงการไม่กี่โครงการที่มีไดเรกทอรี bin บิวด์เกิดขึ้นในไดเร็กทอรีบิลด์จากนั้นไบนารีย้ายออกไปยังไดเร็กทอรี bin หรือไม่ ไบนารีสำหรับการทดสอบและห้องสมุดจะอยู่ในที่เดียวกันหรือไม่

CMake แนะนำบิลด์ "นอกแหล่ง" กล่าวคือคุณสร้างไดเร็กทอรีบิลด์ของคุณเองนอกโปรเจ็กต์และเรียกใช้ CMake จากที่นั่น วิธีนี้จะหลีกเลี่ยงการ "สร้างมลพิษ" ให้กับแผนผังแหล่งที่มาของคุณด้วยไฟล์บิวด์และเป็นที่ต้องการอย่างมากหากคุณใช้ vcs

คุณสามารถระบุได้ว่าไบนารีถูกย้ายหรือคัดลอกไปยังไดเร็กทอรีอื่นเมื่อสร้างขึ้นหรือว่าไบนารีถูกสร้างขึ้นตามค่าเริ่มต้นในไดเร็กทอรีอื่น แต่โดยทั่วไปแล้วไม่จำเป็น CMake มีวิธีที่ครอบคลุมในการติดตั้งโครงการของคุณหากต้องการหรือทำให้โครงการ CMake อื่น ๆ สามารถ "ค้นหา" ไฟล์ที่เกี่ยวข้องของโครงการของคุณได้อย่างง่ายดาย

เกี่ยวกับการสนับสนุนของ CMake ในการค้นหาและดำเนินการทดสอบ gtestสิ่งนี้จะไม่เหมาะสมอย่างยิ่งหากคุณสร้าง gtest เป็นส่วนหนึ่งของโครงการของคุณ FindGtestโมดูลการออกแบบจริงๆที่จะใช้ในกรณีที่ gtest ได้รับการสร้างแยกต่างหากนอกของโครงการของคุณ

CMake มีกรอบการทดสอบของตัวเอง (CTest) และที่ดีที่สุดคือทุกกรณี gtest จะถูกเพิ่มเป็นกรณี CTest

อย่างไรก็ตามGTEST_ADD_TESTSแมโครให้โดยFindGtestจะอนุญาตให้นอกจากง่ายของกรณี gtest กรณี ctest บุคคลค่อนข้างขาดในการที่จะไม่ทำงานสำหรับแมโคร gtest อื่น ๆ กว่าและ TEST มูลค่าหรือType-parameterisedทดสอบใช้, และอื่น ๆ ไม่ได้รับการจัดการที่ทั้งหมดTEST_FTEST_PTYPED_TEST_P

ปัญหาไม่มีทางแก้ง่ายๆอย่างที่ฉันรู้ วิธีที่มีประสิทธิภาพมากที่สุดในการรับรายการ gtest case คือการดำเนินการทดสอบ exe ด้วยแฟล็--gtest_list_testsก อย่างไรก็ตามสามารถทำได้เมื่อสร้าง exe แล้วเท่านั้นดังนั้น CMake จึงไม่สามารถใช้ประโยชน์จากสิ่งนี้ได้ ซึ่งทำให้คุณมีสองทางเลือก CMake ต้องพยายามแยกวิเคราะห์รหัส C ++ เพื่ออนุมานชื่อของการทดสอบ (ไม่สำคัญมากหากคุณต้องการพิจารณามาโคร gtest ทั้งหมดการทดสอบที่แสดงความคิดเห็นการทดสอบที่ปิดใช้งาน) หรือกรณีทดสอบจะถูกเพิ่มเข้าไปใน ไฟล์ CMakeLists.txt

ฉันต้องการใช้ doxygen เพื่อบันทึกรหัสของฉันด้วย เป็นไปได้หรือไม่ที่จะทำให้สิ่งนี้ทำงานโดยอัตโนมัติด้วย cmake และ make?

ใช่ - แม้ว่าฉันจะไม่มีประสบการณ์ด้านนี้ CMake จัดเตรียมFindDoxygenไว้เพื่อการนี้


6

นอกเหนือจากคำตอบอื่น ๆ (ยอดเยี่ยม) แล้วฉันจะอธิบายโครงสร้างที่ฉันใช้สำหรับโครงการขนาดใหญ่
ฉันจะไม่ตอบคำถามย่อยเกี่ยวกับ Doxygen เพราะฉันจะพูดซ้ำสิ่งที่พูดในคำตอบอื่น ๆ


หลักการและเหตุผล

สำหรับความเป็นโมดูลาร์และความสามารถในการบำรุงรักษาโครงการจะจัดเป็นชุดของหน่วยเล็ก ๆ เพื่อความชัดเจนให้ตั้งชื่อว่า UnitX ด้วย X = A, B, C, ... (แต่สามารถมีชื่อทั่วไปก็ได้) จากนั้นโครงสร้างไดเร็กทอรีจะถูกจัดระเบียบเพื่อให้สอดคล้องกับตัวเลือกนี้โดยมีความเป็นไปได้ในการจัดกลุ่มหน่วยหากจำเป็น

สารละลาย

โครงร่างไดเร็กทอรีพื้นฐานมีดังต่อไปนี้ (เนื้อหาของหน่วยจะมีรายละเอียดในภายหลัง):

project
├── CMakeLists.txt
├── UnitA
├── UnitB
├── GroupA
   └── CMakeLists.txt
   └── GroupB
       └── CMakeLists.txt
       └── UnitC
       └── UnitD
   └── UnitE

project/CMakeLists.txt อาจมีสิ่งต่อไปนี้:

cmake_minimum_required(VERSION 3.0.2)
project(project)
enable_testing() # This will be necessary for testing (details below)

add_subdirectory(UnitA)
add_subdirectory(UnitB)
add_subdirectory(GroupA)

และproject/GroupA/CMakeLists.txt:

add_subdirectory(GroupB)
add_subdirectory(UnitE)

และproject/GroupB/CMakeLists.txt:

add_subdirectory(UnitC)
add_subdirectory(UnitD)

ตอนนี้ถึงโครงสร้างของหน่วยต่างๆ (ลองดูตัวอย่างเช่น UnitD)

project/GroupA/GroupB/UnitD
├── README.md
├── CMakeLists.txt
├── lib
   └── CMakeLists.txt
   └── UnitD
       └── ClassA.h
       └── ClassA.cpp
       └── ClassB.h
       └── ClassB.cpp
├── test
   └── CMakeLists.txt
   └── ClassATest.cpp
   └── ClassBTest.cpp
   └── [main.cpp]

ไปยังส่วนประกอบต่างๆ:

  • ฉันชอบมีซอร์ส ( .cpp) และส่วนหัว ( .h) ในโฟลเดอร์เดียวกัน ซึ่งจะหลีกเลี่ยงลำดับชั้นของไดเร็กทอรีที่ซ้ำกันทำให้การบำรุงรักษาง่ายขึ้น สำหรับการติดตั้งก็ไม่มีปัญหา (โดยเฉพาะกับ CMake) แค่กรองไฟล์ส่วนหัว
  • บทบาทของไดเร็กทอรีUnitDคืออนุญาตให้รวมไฟล์ด้วยใน#include <UnitD/ClassA.h>ภายหลัง นอกจากนี้เมื่อติดตั้งยูนิตนี้คุณสามารถคัดลอกโครงสร้างไดเร็กทอรีได้ตามที่เป็นอยู่ โปรดทราบว่าคุณยังสามารถจัดระเบียบไฟล์ต้นฉบับของคุณในไดเรกทอรีย่อย
  • ฉันชอบREADMEไฟล์เพื่อสรุปว่าหน่วยนี้เกี่ยวกับอะไรและระบุข้อมูลที่เป็นประโยชน์เกี่ยวกับมัน
  • CMakeLists.txt อาจมีเพียง:

    add_subdirectory(lib)
    add_subdirectory(test)
  • lib/CMakeLists.txt:

    project(UnitD)
    
    set(headers
        UnitD/ClassA.h
        UnitD/ClassB.h
        )
    
    set(sources
        UnitD/ClassA.cpp
        UnitD/ClassB.cpp    
        )
    
    add_library(${TARGET_NAME} STATIC ${headers} ${sources})
    
    # INSTALL_INTERFACE: folder to which you will install a directory UnitD containing the headers
    target_include_directories(UnitD
                               PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
                               PUBLIC $<INSTALL_INTERFACE:include/SomeDir>
                               )
    
    target_link_libraries(UnitD
                          PUBLIC UnitA
                          PRIVATE UnitC
                          )

    ที่นี่โปรดทราบว่าไม่จำเป็นต้องบอก CMake ว่าเราต้องการไดเร็กทอรีรวมสำหรับUnitAและUnitCเนื่องจากมีการระบุไว้แล้วเมื่อกำหนดค่าหน่วยเหล่านั้น นอกจากนี้PUBLICจะบอกเป้าหมายทั้งหมดที่ขึ้นอยู่กับUnitDว่าควรรวมการUnitAอ้างอิงโดยอัตโนมัติในขณะที่UnitCไม่จำเป็นต้องใช้ ( PRIVATE)

  • test/CMakeLists.txt (ดูเพิ่มเติมด้านล่างหากคุณต้องการใช้ GTest):

    project(UnitDTests)
    
    add_executable(UnitDTests
                   ClassATest.cpp
                   ClassBTest.cpp
                   [main.cpp]
                   )
    
    target_link_libraries(UnitDTests
                          PUBLIC UnitD
    )
    
    add_test(
            NAME UnitDTests
            COMMAND UnitDTests
    )

ใช้ GoogleTest

สำหรับ Google Test วิธีที่ง่ายที่สุดคือหากมีแหล่งที่มาอยู่ในไดเรกทอรีต้นทางของคุณ แต่คุณไม่จำเป็นต้องเพิ่มที่นั่นด้วยตัวเอง ฉันใช้โปรเจ็กต์นี้เพื่อดาวน์โหลดโดยอัตโนมัติและฉันปิดการใช้งานไว้ในฟังก์ชันเพื่อให้แน่ใจว่าดาวน์โหลดเพียงครั้งเดียวแม้ว่าเราจะมีเป้าหมายการทดสอบหลายรายการก็ตาม

ฟังก์ชัน CMake นี้มีดังต่อไปนี้:

function(import_gtest)
  include (DownloadProject)
  if (NOT TARGET gmock_main)
    include(DownloadProject)
    download_project(PROJ                googletest
                     GIT_REPOSITORY      https://github.com/google/googletest.git
                     GIT_TAG             release-1.8.0
                     UPDATE_DISCONNECTED 1
                     )
    set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) # Prevent GoogleTest from overriding our compiler/linker options when building with Visual Studio
    add_subdirectory(${googletest_SOURCE_DIR} ${googletest_BINARY_DIR} EXCLUDE_FROM_ALL)
  endif()
endfunction()

จากนั้นเมื่อฉันต้องการใช้ภายในเป้าหมายการทดสอบของฉันฉันจะเพิ่มบรรทัดต่อไปนี้ในCMakeLists.txt(นี่คือตัวอย่างด้านบนtest/CMakeLists.txt):

import_gtest()
target_link_libraries(UnitDTests gtest_main gmock_main)

"แฮ็ก" ที่ดีที่คุณทำด้วย Gtest และ cmake! ที่เป็นประโยชน์! :)
Tanasis
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.