Tổ chức dự án C ++ (với gtest, cmake và doxygen)


123

Tôi mới làm quen với lập trình nói chung nên tôi quyết định bắt đầu bằng cách tạo một lớp vectơ đơn giản trong C ++. Tuy nhiên, tôi muốn có thói quen tốt ngay từ đầu hơn là cố gắng sửa đổi quy trình làm việc của mình sau này.

Tôi hiện chỉ có hai tệp vector3.hppvector3.cpp. Dự án này sẽ từ từ bắt đầu phát triển (biến nó trở thành một thư viện đại số tuyến tính tổng quát hơn) khi tôi trở nên quen thuộc hơn với mọi thứ, vì vậy tôi muốn áp dụng bố cục dự án "chuẩn" để cuộc sống sau này dễ dàng hơn. Vì vậy, sau khi xem xét xung quanh, tôi đã tìm ra hai cách để tổ chức các tệp hpp và cpp, cách thứ nhất:

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

và bản thể thứ hai:

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

Bạn muốn giới thiệu cái nào và tại sao?

Thứ hai, tôi muốn sử dụng Khung kiểm tra C ++ của Google để kiểm tra đơn vị mã của tôi vì nó có vẻ khá dễ sử dụng. Bạn có đề xuất gói phần mềm này với mã của tôi, chẳng hạn như trong một inc/gtesthoặc contrib/gtestthư mục? Nếu được đóng gói, bạn có đề xuất sử dụng fuse_gtest_files.pytập lệnh để giảm số lượng hoặc tệp hay giữ nguyên? Nếu không được đóng gói thì phần phụ thuộc này được xử lý như thế nào?

Khi nói đến các bài kiểm tra viết, chúng thường được tổ chức như thế nào? Tôi đã nghĩ đến việc có một tệp cpp cho mỗi lớp ( test_vector3.cppví dụ) nhưng tất cả được biên dịch thành một tệp nhị phân để tất cả chúng có thể được chạy cùng nhau một cách dễ dàng?

Vì thư viện gtest thường được xây dựng bằng cmake và make, tôi đã nghĩ rằng dự án của tôi cũng được xây dựng như thế này có hợp lý không? Nếu tôi quyết định sử dụng bố cục dự án sau:

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

Nó sẽ CMakeLists.txtphải trông như thế nào để nó có thể chỉ xây dựng thư viện hoặc thư viện và các bài kiểm tra? Ngoài ra, tôi đã thấy khá nhiều dự án có một buildvà một binthư mục. Quá trình xây dựng có xảy ra trong thư mục xây dựng và sau đó các tệp nhị phân được chuyển đến thư mục bin không? Các mã nhị phân cho các bài kiểm tra và thư viện có ở cùng một nơi không? Hoặc sẽ có ý nghĩa hơn nếu cấu trúc nó như sau:

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

Tôi cũng muốn sử dụng doxygen để ghi lại mã của mình. Có thể làm cho điều này tự động chạy với cmake và make không?

Xin lỗi vì rất nhiều câu hỏi, nhưng tôi chưa tìm được cuốn sách nào về C ++ trả lời thỏa đáng những câu hỏi kiểu này.


6
Câu hỏi hay, nhưng tôi không nghĩ nó phù hợp với định dạng Hỏi và Đáp của Stack Overflow . Tôi rất quan tâm đến một câu trả lời. 1 & fav
Luchian Grigore

1
Đây là những câu hỏi rất lớn. Có thể tốt hơn là chia nó thành nhiều câu hỏi nhỏ hơn và đặt các liên kết với nhau. Dù sao để trả lời phần cuối cùng: Với CMake, bạn có thể chọn xây dựng bên trong và bên ngoài thư mục src của mình (tôi khuyên bạn nên ở bên ngoài). Và có, bạn có thể sử dụng doxygen với CMake tự động.
nhầm vào

Câu trả lời:


84

Các hệ thống xây dựng C ++ có một chút nghệ thuật đen và dự án càng cũ thì bạn càng có thể tìm thấy nhiều thứ kỳ lạ hơn nên không có gì ngạc nhiên khi có rất nhiều câu hỏi được đưa ra. Tôi sẽ thử lần lượt xem qua các câu hỏi và đề cập đến một số điều chung về việc xây dựng thư viện C ++.

Tách tiêu đề và tệp cpp trong thư mục. Điều này chỉ cần thiết nếu bạn đang xây dựng một thành phần được cho là dùng làm thư viện thay vì một ứng dụng thực tế. Tiêu đề của bạn là cơ sở để người dùng tương tác với những gì bạn cung cấp và phải được cài đặt. Điều này có nghĩa là chúng phải nằm trong một thư mục con (không ai muốn nhiều tiêu đề kết thúc ở cấp cao nhất /usr/include/) và các tiêu đề của bạn phải có thể tự bao gồm chúng với thiết lập như vậy.

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

hoạt động tốt, bởi vì bao gồm các đường dẫn hoạt động và bạn có thể sử dụng dễ dàng lấp lánh cho các mục tiêu cài đặt.

Nhóm các phần phụ thuộc: Tôi nghĩ điều này phần lớn phụ thuộc vào khả năng của hệ thống xây dựng trong việc định vị và định cấu hình các phần phụ thuộc cũng như mức độ phụ thuộc của mã của bạn vào một phiên bản duy nhất. Nó cũng phụ thuộc vào khả năng của người dùng của bạn và mức độ phụ thuộc dễ cài đặt trên nền tảng của họ. CMake đi kèm với một find_packagetập lệnh cho Google Test. Điều này làm cho mọi thứ dễ dàng hơn rất nhiều. Tôi sẽ chỉ gói khi cần thiết và tránh nó nếu không.

Cách xây dựng: Tránh các bản dựng trong nguồn. CMake làm cho việc xây dựng nguồn trở nên dễ dàng và nó làm cho cuộc sống dễ dàng hơn rất nhiều.

Tôi cho rằng bạn cũng muốn sử dụng CTest để chạy thử nghiệm cho hệ thống của mình (nó cũng đi kèm với hỗ trợ tích hợp cho GTest). Một quyết định quan trọng cho việc bố trí thư mục và tổ chức thử nghiệm sẽ là: Bạn có kết thúc với các dự án con không? Nếu vậy, bạn cần thêm một số công việc khi thiết lập CMakeLists và nên chia các dự án con của bạn thành các thư mục con, mỗi thư mục có tệp includevà riêng src. Thậm chí có thể chạy và xuất doxygen của riêng họ (kết hợp nhiều dự án doxygen là có thể, nhưng không dễ hoặc không đẹp).

Bạn sẽ kết thúc với một cái gì đó như thế này:

└── 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

Ở đâu

  • (1) định cấu hình các phần phụ thuộc, thông số kỹ thuật nền tảng và đường dẫn đầu ra
  • (2) cấu hình thư viện bạn sẽ xây dựng
  • (3) cấu hình các tệp thực thi thử nghiệm và các trường hợp thử nghiệm

Trong trường hợp bạn có các thành phần phụ, tôi khuyên bạn nên thêm một hệ thống phân cấp khác và sử dụng cây ở trên cho mỗi dự án con. Sau đó, mọi thứ trở nên phức tạp, bởi vì bạn cần phải quyết định xem các thành phần phụ có tìm kiếm và định cấu hình các phần phụ thuộc của chúng hay không hoặc nếu bạn làm điều đó ở cấp cao nhất. Điều này cần được quyết định trong từng trường hợp cụ thể.

Doxygen: Sau khi bạn vượt qua được bước nhảy cấu hình của doxygen, việc sử dụng CMake add_custom_commandđể thêm mục tiêu doc ​​là điều dễ hiểu.

Đây là cách các dự án của tôi kết thúc và tôi đã thấy một số dự án rất giống nhau, nhưng tất nhiên đây không phải là cách chữa trị tất cả.

Phụ lục Tại một số điểm, bạn sẽ muốn tạo một config.hpp tệp có chứa phiên bản xác định và có thể là xác định cho một số định danh điều khiển phiên bản (mã băm Git hoặc số sửa đổi SVN). CMake có các mô-đun để tự động tìm kiếm thông tin đó và tạo tệp. Bạn có thể sử dụng CMake configure_fileđể thay thế các biến trong tệp mẫu bằng các biến được xác định bên trong CMakeLists.txt.

Nếu bạn đang xây dựng thư viện, bạn cũng sẽ cần một định nghĩa xuất khẩu để có được sự khác biệt giữa các trình biên dịch, ví dụ như __declspectrên MSVC và visibilitycác thuộc tính trên GCC / clang.


2
Câu trả lời hay, nhưng tôi vẫn chưa rõ tại sao bạn cần đặt các tệp tiêu đề của mình vào một thư mục con có tên dự án bổ sung: "/prj/include/prj/foo.hpp", điều này có vẻ thừa đối với tôi. Tại sao không chỉ "/prj/include/foo.hpp"? Tôi giả định rằng bạn sẽ có cơ hội sắp xếp lại các thư mục cài đặt tại thời điểm cài đặt, vì vậy bạn nhận được <INSTALL_DIR> /include/prj/foo.hpp khi cài đặt, hoặc điều đó có khó khăn trong CMake không?
William Payne

6
@William Điều đó thực sự khó thực hiện với CPack. Ngoài ra, bao gồm bên trong tệp nguồn của bạn trông như thế nào? Nếu chúng chỉ là "header.hpp" trên phiên bản đã cài đặt "/ usr / include / prj /" cần phải ở trong đường dẫn bao gồm trái ngược với chỉ "/ usr / include".
pmr

37

Là một người mới bắt đầu, có một số tên thông thường cho các thư mục mà bạn không thể bỏ qua, chúng dựa trên truyền thống lâu đời với hệ thống tệp Unix. Đó là:

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

Có lẽ là một ý tưởng hay nếu bạn tuân theo bố cục cơ bản này, ít nhất là ở cấp cao nhất.

Về việc tách tệp tiêu đề và tệp nguồn (cpp), cả hai lược đồ đều khá phổ biến. Tuy nhiên, tôi có xu hướng muốn giữ chúng lại với nhau, nó chỉ thực tế hơn trong các công việc hàng ngày để có các tệp cùng nhau. Ngoài ra, khi tất cả mã nằm trong một thư mục cấp cao nhất, tức là trunk/src/thư mục, bạn có thể nhận thấy rằng tất cả các thư mục khác (bin, lib, include, doc và có thể một số thư mục thử nghiệm) ở cấp cao nhất, ngoài thư mục "xây dựng" cho một bản dựng ngoài nguồn, là tất cả các thư mục không chứa gì khác ngoài các tệp được tạo trong quá trình xây dựng. Và do đó, chỉ thư mục src cần được sao lưu, hoặc tốt hơn nhiều, được giữ dưới hệ thống / máy chủ kiểm soát phiên bản (như Git hoặc SVN).

Và khi nói đến việc cài đặt các tệp tiêu đề của bạn trên hệ thống đích (nếu bạn muốn cuối cùng phân phối thư viện của mình), thì CMake có một lệnh để cài đặt tệp (ngầm tạo một mục tiêu "cài đặt", để thực hiện "thực hiện cài đặt"). bạn có thể sử dụng để đặt tất cả các tiêu đề vào /usr/include/thư mục. Tôi chỉ sử dụng macro cmake sau cho mục đích này:

# 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)

Đâu SRCROOTlà một biến cmake mà tôi đặt vào thư mục src và INCLUDEROOTlà biến cmake mà tôi định cấu hình ở bất cứ đâu mà tiêu đề cần đến. Tất nhiên, có nhiều cách khác để làm điều này, và tôi chắc chắn rằng cách của tôi không phải là tốt nhất. Vấn đề là, không có lý do gì để tách tiêu đề và nguồn chỉ vì chỉ có tiêu đề cần được cài đặt trên hệ thống đích, vì rất dễ dàng, đặc biệt là với CMake (hoặc CPack), để chọn và định cấu hình tiêu đề để được cài đặt mà không cần phải có chúng trong một thư mục riêng. Và đây là những gì tôi đã thấy ở hầu hết các thư viện.

Trích dẫn: Thứ hai, tôi muốn sử dụng Khung kiểm tra C ++ của Google để kiểm tra đơn vị mã của tôi vì nó có vẻ khá dễ sử dụng. Bạn có đề xuất gói điều này với mã của tôi, ví dụ: trong thư mục "inc / gtest" hoặc "Contrib / gtest" không? Nếu được đóng gói, bạn có đề xuất sử dụng tập lệnh fuse_gtest_files.py để giảm số lượng tệp hoặc giữ nguyên? Nếu không được đóng gói thì phần phụ thuộc này được xử lý như thế nào?

Không gói các phụ thuộc với thư viện của bạn. Nói chung, đây là một ý tưởng khá kinh khủng, và tôi luôn ghét nó khi mắc kẹt khi cố gắng xây dựng một thư viện đã làm được điều đó. Đó nên là phương sách cuối cùng của bạn, và hãy cẩn thận với những cạm bẫy. Thông thường, mọi người kết hợp các phần phụ thuộc với thư viện của họ bởi vì họ nhắm mục tiêu đến một môi trường phát triển tồi tệ (ví dụ: Windows) hoặc vì họ chỉ hỗ trợ phiên bản cũ (không được dùng nữa) của thư viện (phụ thuộc) được đề cập. Cạm bẫy chính là phần phụ thuộc đi kèm của bạn có thể xung đột với các phiên bản đã được cài đặt của cùng một thư viện / ứng dụng (ví dụ: bạn đã gói gtest, nhưng người đang cố gắng xây dựng thư viện của bạn đã cài đặt phiên bản gtest mới hơn (hoặc cũ hơn), thì cả hai có thể xung đột và khiến người đó đau đầu rất khó chịu). Vì vậy, như tôi đã nói, hãy tự chịu rủi ro, và tôi chỉ nói như một phương sách cuối cùng. Yêu cầu mọi người cài đặt một số phụ thuộc trước khi có thể biên dịch thư viện của bạn là một việc ít ác hơn nhiều so với việc cố gắng giải quyết các xung đột giữa các phụ thuộc đi kèm và các cài đặt hiện có.

Trích dẫn: Khi nói đến các bài kiểm tra viết, chúng thường được tổ chức như thế nào? Tôi đã nghĩ đến việc có một tệp cpp cho mỗi lớp (ví dụ: test_vector3.cpp) nhưng tất cả được biên dịch thành một tệp nhị phân để tất cả chúng có thể được chạy cùng nhau một cách dễ dàng?

Theo ý kiến ​​của tôi, một tệp cpp cho mỗi lớp (hoặc một nhóm nhỏ gắn kết các lớp và chức năng) là thông thường và thiết thực hơn. Tuy nhiên, chắc chắn, đừng biên dịch tất cả chúng thành một tệp nhị phân chỉ để "tất cả chúng có thể chạy cùng nhau". Đó là một ý tưởng thực sự tồi. Nói chung, khi nói đến mã hóa, bạn muốn chia nhỏ mọi thứ ra sao cho hợp lý. Trong trường hợp kiểm tra đơn vị, bạn không muốn một tệp nhị phân chạy tất cả kiểm tra, vì điều đó có nghĩa là bất kỳ thay đổi nhỏ nào bạn thực hiện đối với bất kỳ thứ gì trong thư viện của mình đều có khả năng gây ra sự biên dịch lại gần như hoàn toàn của chương trình kiểm tra đơn vị đó và đó chỉ là phút / giờ bị mất khi chờ biên dịch lại. Chỉ cần tuân theo một sơ đồ đơn giản: 1 đơn vị = 1 chương trình kiểm tra đơn vị. Sau đó,

Trích dẫn: Vì thư viện gtest thường được xây dựng bằng cmake và make, tôi đã nghĩ rằng dự án của tôi cũng được xây dựng như thế này có hợp lý không? Nếu tôi quyết định sử dụng bố cục dự án sau:

Tôi muốn đề xuất bố cục này:

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

Một số điều cần lưu ý ở đây. Đầu tiên, các thư mục con của thư mục src của bạn phải phản chiếu các thư mục con của thư mục include của bạn, điều này chỉ để giữ cho mọi thứ trực quan (ngoài ra, hãy cố gắng giữ cho cấu trúc thư mục con của bạn phẳng hợp lý (nông), vì lồng sâu các thư mục thường phức tạp hơn bất cứ điều gì khác). Thứ hai, thư mục "include" chỉ là một thư mục cài đặt, nội dung của nó chỉ là bất kỳ tiêu đề nào được chọn ra từ thư mục src.

Thứ ba, hệ thống CMake được thiết kế để được phân phối trên các thư mục con nguồn, không phải dưới dạng một tệp CMakeLists.txt ở cấp cao nhất. Điều này giữ cho mọi thứ cục bộ và điều đó tốt (trên tinh thần chia nhỏ mọi thứ thành các phần độc lập). Nếu bạn thêm nguồn mới, tiêu đề mới hoặc chương trình thử nghiệm mới, tất cả những gì bạn cần là chỉnh sửa một tệp CMakeLists.txt nhỏ và đơn giản trong thư mục con được đề cập mà không ảnh hưởng đến bất kỳ thứ gì khác. Điều này cũng cho phép bạn cấu trúc lại các thư mục một cách dễ dàng (CMakeLists là cục bộ và chứa trong các thư mục con đang được di chuyển). CMakeLists cấp cao nhất nên chứa hầu hết các cấu hình cấp cao nhất, chẳng hạn như thiết lập thư mục đích, lệnh tùy chỉnh (hoặc macro) và tìm các gói được cài đặt trên hệ thống. CMakeLists cấp thấp hơn chỉ nên chứa các danh sách đơn giản gồm các tiêu đề, nguồn,

Trích dẫn: CMakeLists.txt sẽ phải trông như thế nào để nó có thể chỉ xây dựng thư viện hoặc thư viện và các bài kiểm tra?

Câu trả lời cơ bản là CMake cho phép bạn loại trừ cụ thể các mục tiêu nhất định khỏi "tất cả" (là những gì được tạo khi bạn nhập "make") và bạn cũng có thể tạo các gói mục tiêu cụ thể. Tôi không thể thực hiện hướng dẫn CMake ở đây, nhưng bạn có thể tự tìm hiểu khá dễ dàng. Tuy nhiên, trong trường hợp cụ thể này, giải pháp được đề xuất tất nhiên là sử dụng CTest, chỉ là một tập hợp lệnh bổ sung mà bạn có thể sử dụng trong tệp CMakeLists để đăng ký một số mục tiêu (chương trình) được đánh dấu là đơn vị- các bài kiểm tra. Vì vậy, CMake sẽ đặt tất cả các bài kiểm tra trong một danh mục bản dựng đặc biệt và đó chính xác là những gì bạn yêu cầu, do đó, vấn đề đã được giải quyết.

Trích dẫn: Ngoài ra, tôi đã thấy khá nhiều dự án có quảng cáo xây dựng một thư mục bin. Quá trình xây dựng có xảy ra trong thư mục xây dựng và sau đó các tệp nhị phân được chuyển đến thư mục bin không? Các mã nhị phân cho các bài kiểm tra và thư viện có ở cùng một nơi không? Hoặc sẽ có ý nghĩa hơn nếu cấu trúc nó như sau:

Có một thư mục xây dựng bên ngoài nguồn (xây dựng "ngoài nguồn") thực sự là điều cần làm duy nhất, đó là tiêu chuẩn thực tế ngày nay. Vì vậy, chắc chắn, có một thư mục "xây dựng" riêng, bên ngoài thư mục nguồn, giống như những người CMake đề xuất, và như mọi lập trình viên tôi từng gặp. Đối với thư mục bin, đó là một quy ước, và có lẽ là một ý tưởng tốt để tuân theo nó, như tôi đã nói ở phần đầu của bài viết này.

Trích dẫn: Tôi cũng muốn sử dụng doxygen để ghi lại mã của mình. Có thể làm cho điều này tự động chạy với cmake và make không?

Đúng. Nó là nhiều hơn có thể, nó là tuyệt vời. Tùy thuộc vào mức độ ưa thích mà bạn muốn có, có một số khả năng. CMake có một mô-đun cho Doxygen (tức là, find_package(Doxygen)) cho phép bạn đăng ký các mục tiêu sẽ chạy Doxygen trên một số tệp. Nếu bạn muốn làm những điều thú vị hơn, như cập nhật số phiên bản trong Doxyfile hoặc tự động nhập ngày tháng / tem tác giả cho các tệp nguồn, v.v., tất cả đều có thể thực hiện được với một chút CMake kung-fu. Nói chung, làm điều này sẽ liên quan đến việc bạn giữ một Doxyfile nguồn (ví dụ: "Doxyfile.in" mà tôi đặt trong bố cục thư mục ở trên) có mã thông báo được tìm thấy và thay thế bằng các lệnh phân tích cú pháp của CMake. Trong tệp CMakeLists cấp cao nhất của tôi , bạn sẽ tìm thấy một đoạn CMake kung-fu thực hiện một số điều thú vị với cmake-doxygen cùng nhau.


Vì vậy, main.cppnên đi đến trunk/bin?
Ugnius Malūkas

17

Cấu trúc dự án

Tôi thường thích những điều sau:

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

Điều này có nghĩa là bạn có một tập hợp các tệp API được xác định rất rõ ràng cho thư viện của mình và cấu trúc có nghĩa là các ứng dụng khách của thư viện của bạn sẽ làm

#include "project/vector3.hpp"

thay vì ít rõ ràng hơn

#include "vector3.hpp"


Tôi thích cấu trúc của cây / src khớp với cấu trúc của cây / include, nhưng đó thực sự là sở thích cá nhân. Tuy nhiên, nếu dự án của bạn mở rộng để chứa các thư mục con bên trong / include / project, nó thường giúp khớp với các thư mục bên trong cây / src.

Đối với các bài kiểm tra, tôi ưu tiên giữ chúng "gần" với các tệp mà chúng kiểm tra và nếu bạn kết thúc với các thư mục con bên trong / src, đó là một mô hình khá dễ dàng để người khác làm theo nếu họ muốn tìm mã kiểm tra của tệp nhất định.


Thử nghiệm

Thứ hai, tôi muốn sử dụng Khung kiểm tra C ++ của Google để kiểm tra đơn vị mã của tôi vì nó có vẻ khá dễ sử dụng.

Gtest thực sự đơn giản để sử dụng và khá toàn diện về khả năng của nó. Nó có thể được sử dụng cùng với gmock rất dễ dàng để mở rộng khả năng của nó, nhưng trải nghiệm của riêng tôi với gmock không mấy thuận lợi. Tôi khá sẵn sàng để chấp nhận rằng điều này có thể là do những thiếu sót của riêng tôi, nhưng các thử nghiệm gmock có xu hướng khó tạo hơn và dễ hỏng / khó bảo trì hơn nhiều. Một cái đinh lớn trong quan tài gmock là nó thực sự không chơi đẹp với những con trỏ thông minh.

Đây là một câu trả lời rất tầm thường và chủ quan cho một câu hỏi lớn (có thể không thực sự thuộc về SO)

Bạn có đề xuất gói điều này với mã của tôi, ví dụ: trong thư mục "inc / gtest" hoặc "Contrib / gtest" không? Nếu được đóng gói, bạn có đề xuất sử dụng tập lệnh fuse_gtest_files.py để giảm số lượng tệp hoặc giữ nguyên? Nếu không được đóng gói thì phần phụ thuộc này được xử lý như thế nào?

Tôi thích sử dụng ExternalProject_Addmô-đun của CMake hơn . Điều này tránh cho bạn phải giữ mã nguồn gtest trong kho của bạn hoặc cài đặt nó ở bất cứ đâu. Nó được tải xuống và tích hợp tự động trong cây xây dựng của bạn.

Xem câu trả lời của tôi về các chi tiết cụ thể ở đây .

Khi nói đến các bài kiểm tra viết, chúng thường được tổ chức như thế nào? Tôi đã nghĩ đến việc có một tệp cpp cho mỗi lớp (ví dụ: test_vector3.cpp) nhưng tất cả được biên dịch thành một tệp nhị phân để tất cả chúng có thể được chạy cùng nhau một cách dễ dàng?

Kế hoạch tốt.


Xây dựng

Tôi là một fan hâm mộ của CMake, nhưng đối với các câu hỏi liên quan đến bài kiểm tra của bạn, SO có lẽ không phải là nơi tốt nhất để hỏi ý kiến ​​về một vấn đề chủ quan như vậy.

CMakeLists.txt sẽ phải trông như thế nào để nó có thể chỉ xây dựng thư viện hoặc thư viện và các bài kiểm tra?

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

Thư viện sẽ xuất hiện dưới dạng "ProjectLibrary" đích và bộ thử nghiệm dưới dạng "ProjectTest" đích. Bằng cách chỉ định thư viện như một phần phụ thuộc của exe thử nghiệm, việc xây dựng exe thử nghiệm sẽ tự động khiến thư viện được xây dựng lại nếu nó đã lỗi thời.

Ngoài ra, tôi đã thấy khá nhiều dự án có quảng cáo xây dựng một thư mục bin. Quá trình xây dựng có xảy ra trong thư mục xây dựng và sau đó các tệp nhị phân được chuyển đến thư mục bin không? Các mã nhị phân cho các bài kiểm tra và thư viện có ở cùng một nơi không?

CMake đề xuất các bản dựng "ngoài nguồn", tức là bạn tạo thư mục bản dựng của riêng mình bên ngoài dự án và chạy CMake từ đó. Điều này tránh "làm ô nhiễm" cây nguồn của bạn với các tệp xây dựng và rất đáng được mong đợi nếu bạn đang sử dụng vcs.

Bạn có thể chỉ định rằng các tệp nhị phân được di chuyển hoặc sao chép vào một thư mục khác sau khi được tạo hoặc chúng được tạo theo mặc định trong một thư mục khác, nhưng nói chung là không cần. CMake cung cấp các cách toàn diện để cài đặt dự án của bạn nếu muốn hoặc giúp các dự án CMake khác dễ dàng "tìm thấy" các tệp liên quan của dự án của bạn.

Liên quan đến sự hỗ trợ riêng của CMake để tìm và thực hiện các bài kiểm tra gtest , điều này phần lớn sẽ không phù hợp nếu bạn xây dựng gtest như một phần của dự án của mình. Các FindGtestmô-đun thực sự được thiết kế để sử dụng trong trường hợp gtest đã được xây dựng riêng biệt bên ngoài của dự án của bạn.

CMake cung cấp khung thử nghiệm riêng (CTest) và lý tưởng là mọi trường hợp gtest sẽ được thêm vào làm trường hợp CTest.

Tuy nhiên, GTEST_ADD_TESTSmacro được cung cấp bởi FindGtestđể cho phép dễ dàng bổ sung các trường hợp gtest vì các trường hợp ctest riêng lẻ hơi thiếu ở chỗ nó không hoạt động cho các macro của gtest ngoài TESTTEST_F. Giá trị gia tăng hay Type-parameterised kiểm tra bằng cách sử dụng TEST_P, TYPED_TEST_Pvv không được xử lý ở tất cả.

Vấn đề không có một giải pháp dễ dàng mà tôi biết. Cách mạnh mẽ nhất để có được danh sách các trường hợp gtest là thực thi exe thử nghiệm với cờ --gtest_list_tests. Tuy nhiên, điều này chỉ có thể được thực hiện khi exe được xây dựng, vì vậy CMake không thể sử dụng điều này. Điều này khiến bạn có hai sự lựa chọn; CMake phải cố gắng phân tích cú pháp mã C ++ để suy ra tên của các bài kiểm tra (không quá tầm thường nếu bạn muốn tính đến tất cả các macro gtest, các bài kiểm tra đã nhận xét, bài kiểm tra bị vô hiệu hóa) hoặc các trường hợp kiểm tra được thêm thủ công vào Tệp CMakeLists.txt.

Tôi cũng muốn sử dụng doxygen để ghi lại mã của mình. Có thể làm cho điều này tự động chạy với cmake và make không?

Có - mặc dù tôi không có kinh nghiệm về mặt này. CMake cung cấp FindDoxygencho mục đích này.


6

Ngoài các câu trả lời (tuyệt vời) khác, tôi sẽ mô tả một cấu trúc mà tôi đã sử dụng cho các dự án quy mô tương đối lớn .
Tôi sẽ không giải quyết câu hỏi con về Doxygen, vì tôi sẽ chỉ lặp lại những gì được nói trong các câu trả lời khác.


Cơ sở lý luận

Đối với tính mô-đun và khả năng bảo trì, dự án được tổ chức như một tập hợp các đơn vị nhỏ. Để rõ ràng, hãy đặt tên chúng là UnitX, với X = A, B, C, ... (nhưng chúng có thể có bất kỳ tên chung nào). Cấu trúc thư mục sau đó được tổ chức để phản ánh sự lựa chọn này, với khả năng nhóm các đơn vị nếu cần thiết.

Giải pháp

Bố cục thư mục cơ bản như sau (nội dung của các đơn vị sẽ được trình bày chi tiết ở phần sau):

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

project/CMakeLists.txt có thể chứa những thứ sau:

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)

Bây giờ đến cấu trúc của các đơn vị khác nhau (hãy lấy ví dụ, 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]

Đối với các thành phần khác nhau:

  • Tôi thích có source ( .cpp) và headers ( .h) trong cùng một thư mục. Điều này tránh trùng lặp phân cấp thư mục, bảo trì dễ dàng hơn. Để cài đặt, không có vấn đề gì (đặc biệt là với CMake) chỉ cần lọc các tệp tiêu đề.
  • Vai trò của thư UnitDmục sau này là cho phép bao gồm các tệp với #include <UnitD/ClassA.h>. Ngoài ra, khi cài đặt đơn vị này, bạn chỉ có thể sao chép cấu trúc thư mục như cũ. Lưu ý rằng bạn cũng có thể sắp xếp các tệp nguồn của mình trong các thư mục con.
  • Tôi thích một READMEtệp để tóm tắt nội dung của đơn vị và nêu rõ thông tin hữu ích về nó.
  • CMakeLists.txt chỉ có thể chứa:

    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
                          )

    Ở đây, lưu ý rằng không cần thiết phải nói với CMake rằng chúng tôi muốn các thư mục bao gồm UnitAUnitC, vì điều này đã được chỉ định khi định cấu hình các đơn vị đó. Ngoài ra, PUBLICsẽ cho tất cả các mục tiêu phụ thuộc vào UnitDrằng chúng sẽ tự động bao gồm phần UnitAphụ thuộc, trong khi đó UnitCsẽ không bắt buộc ( PRIVATE).

  • test/CMakeLists.txt (xem thêm bên dưới nếu bạn muốn sử dụng GTest cho nó):

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

Sử dụng GoogleTest

Đối với Google Test, cách dễ nhất là nếu nguồn của nó có trong thư mục nguồn của bạn ở đâu đó, nhưng bạn không cần phải tự thêm nó vào đó. Tôi đã sử dụng dự án này để tải xuống tự động và tôi gói cách sử dụng của nó trong một chức năng để đảm bảo rằng nó chỉ được tải xuống một lần, mặc dù chúng tôi có một số mục tiêu thử nghiệm.

Chức năng CMake này như sau:

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()

và sau đó, khi tôi muốn sử dụng nó bên trong một trong các mục tiêu thử nghiệm của mình, tôi sẽ thêm các dòng sau vào CMakeLists.txt(đây là ví dụ ở trên, test/CMakeLists.txt):

import_gtest()
target_link_libraries(UnitDTests gtest_main gmock_main)

Tuyệt vời "hack" bạn đã làm ở đó với Gtest và cmake! Hữu ích! :)
Tanasis
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.