24
\$\begingroup\$

UPDATE: I am no longer updating this post with the contents of my actual project since they have changed significantly (Unit testing is now done with doctest, code coverage is included, etc.). This post is still complete as it is and should suffice to serve as a starting point for new C++14 projects.


Recently, I have decided to create a C++14 oroject template that I plan on using for all of my C++ projects. The build system generator I use here is CMake. Here were my goals when I set this up:

  1. The project should compile with MSVC, GCC, and Clang.
  2. I can test the code on all platforms.
  3. I can install the targets on all platforms.
  4. I can generate Documentation on all platforms using Doxygen.

Here is my project structure:

project-name/
├── CMakeLists.txt
├── cmake
│   └── Modules
│       └── ParseAndAddCatchTests.cmake
├── doc
│   ├── CMakeLists.txt
│   ├── Doxyfile.in
│   └── main_page.md
├── include
│   └── project-abbr
│       ├── config.hpp <-- Contains project versioning
│       └── factorial.hpp
├── src
│   ├── CMakeLists.txt
│   ├── factorial.cpp
│   └── main.cpp
├── test
│   ├── CMakeLists.txt
│   ├── factorial_test.cpp
│   └── test_runner.cpp
└── third_party
    └── catch
        └── CMakeLists.txt

Here is my top level CMakeLists.txt:

cmake_minimum_required(VERSION 3.1)

if (POLICY CMP0048)
    cmake_policy(SET CMP0048 NEW)
endif (POLICY CMP0048)

project(Project-Name VERSION 0.1.0)

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Options
option(BUILD_TESTS "Build test executable" OFF)
option(GEN_DOCS "Generate documentation" OFF)

set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/")

if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
    message(STATUS "Setting build type to 'Release' as no build type was specified")
    set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the build type (Debug/Release)" FORCE)
endif (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)

if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror")
    set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g")
    set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2")
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /WX")
    set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /O2")
endif(CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU")

include_directories(include)
add_subdirectory(src)

if (BUILD_TESTS)
    add_subdirectory(third_party/catch)
    include(CTest)
    enable_testing()
    add_subdirectory(test)
endif (BUILD_TESTS)

if (GEN_DOCS)
    add_subdirectory(doc)
endif (GEN_DOCS)

# Install the project header files into the appropriate directory
# Other installs are in src/CMakeLists.txt
install(DIRECTORY include/ DESTINATION include)

The CMakeLists.txt in src:

add_executable(Project-Name main.cpp) # The main executable
add_library(Project-Name-lib hello_world.cpp factorial.cpp) # A library for tests

SET_TARGET_PROPERTIES(Project-Name-lib PROPERTIES PREFIX "") # Remove the lib prefix

target_link_libraries(Project-Name Project-Name-lib) # Link our sources to the executable

install(TARGETS Project-Name DESTINATION bin)
install(TARGETS Project-Name-lib
    ARCHIVE DESTINATION lib
    LIBRARY DESTINATION lib
    RUNTIME DESTINATION bin
)

The CMakeLists.txt in test:

set(TEST_SOURCES factorial_test.cpp hello_world_test.cpp)

add_executable(test_runner test_runner.cpp ${TEST_SOURCES})
target_link_libraries(test_runner Project-Name-lib)
target_include_directories(test_runner PRIVATE ${CATCH_INCLUDE_DIR} ${COMMON_INCLUDES})

include(ParseAndAddCatchTests)
ParseAndAddCatchTests(test_runner)

The CMakeLists.txt in third_party/catch (Taken from official docs and slightly modified):

include(ExternalProject)
find_package(Git REQUIRED)

ExternalProject_Add(
    catch
    PREFIX ${CMAKE_BINARY_DIR}/catch
    GIT_REPOSITORY https://github.com/catchorg/Catch2.git
    TIMEOUT 10
    UPDATE_COMMAND ${GIT_EXECUTABLE} pull
    CONFIGURE_COMMAND ""
    BUILD_COMMAND ""
    INSTALL_COMMAND ""
    LOG_DOWNLOAD ON
)

# Expose required variable (CATCH_INCLUDE_DIR) to parent scope
ExternalProject_Get_Property(catch source_dir)
set(CATCH_INCLUDE_DIR ${source_dir}/single_include CACHE INTERNAL "Path to include folder for Catch")

The CMakeLists.txt in doc:

find_package(Doxygen)

if (DOXYGEN_FOUND)
    set(DOXYGEN_IN Doxyfile.in)
    set(DOXYGEN_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)

    configure_file(${DOXYGEN_IN} ${DOXYGEN_OUT} @ONLY)

    add_custom_target(doc
        COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_OUT}
        WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
        VERBATIM
    )
else (DOXYGEN_FOUND)
    message(FATAL_ERROR "Doxygen needs to be installed to generate documentation!")
endif (DOXYGEN_FOUND)

Here are the questions I want to be answered from this review:

  1. Am I following the best practices in terms of setting up my project?
  2. Is there any way my CMake syntax could be improved upon?
  3. Is my code suitable for Clang, GCC, and MSVC? (I have tested using Travis CI and Appveyor, but I just need to make sure)
  4. Am I installing all of my targets correctly (In the right directories)?
  5. How would I handle resource files in this setup? Suppose a file in src needed a file in src/resources/. Then where would this file be installed? In bin or somewhere else? (Full SO Question)
\$\endgroup\$
3
  • \$\begingroup\$ Is ParseAndAddCatchTests.cmake left out intentionally? Also, do you want to support other c++ compilers, or just those three? \$\endgroup\$
    – hoffmale
    Mar 22 '18 at 7:41
  • \$\begingroup\$ @hoffmale ParseAndAddCatchTests.cmake was provided by Catch and my verbatim usage of that file led me to leave it out. My primary goal is to support those 3 compilers, but I wouldn't mind if I was able to support more... ;) \$\endgroup\$ Mar 22 '18 at 11:24
  • \$\begingroup\$ For reference, here is ParseAndAddCatchTests.cmake. \$\endgroup\$ Mar 22 '18 at 11:27
2
\$\begingroup\$

There's not much to improve. I would like to see actual TARGET names instead of Project-Name, but overall this is a well-readable and well structured CMake project. The minimum CMake version 3.1 is a blast from the past, but it's great to see an appropriate minimum version if no recent features are used and/or necessary. Kudos for that!

  1. Am I following the best practices in terms of setting up my project?

Yes, except for a single option, namely BUILD_TESTS and the test section:

if (BUILD_TESTS)
    add_subdirectory(third_party/catch)
    include(CTest)
    enable_testing()
    add_subdirectory(test)
endif (BUILD_TESTS)

The CTest module itself contains an option BUILD_TESTING and calls enable_testing() if BUILD_TESTING=ON. While this has only been documented in CMake 3.15 and higher, it has been active since at least 2005. You can therefore simplify the above section to

include(CTest)
if(BUILD_TESTING)
    add_subdirectory(third_party/catch)
    add_subdirectory(test)
endif (BUILD_TESTING)

We can then also get rid of our own option(BUILD_TESTS ...).

  1. Is there any way my CMake syntax could be improved upon?

Not much. CMake syntax is always influenced by personal preference. I find your configuration easy to read and quite navigable, so I say it's perfectly fine. All other "improvements" would be just my personal preference. One part of that preference is to get rid of the expression in endif(...) on short blocks; but that's only my preference.

  1. Is my code suitable for Clang, GCC, and MSVC? (I have tested using Travis CI and Appveyor, but I just need to make sure)

Yes, but your compiler check is a little bit more involved than necessary. CMake provides MSVC to check whether Microsoft Visual C(++) is active. You can therefore relax the test to

if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror")
    set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g")
    set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2")
elseif (MSVC)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /WX")
    set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /O2")
endif(CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.