Modern Development Environment for C++: Part 2

In Part 1 of this series, I gave a quick overview of each tool in the development environment and how I came to the conclusion that the tool was a good fit. Today I’m going to begin showing you how these tools fit together in the complex puzzle that is a C++ development environment.

We’ll walk through creating a skeleton C++ project from the ground up and incorporate each tool, one-by-one. Each step of this process is recorded as a commit in Git at https://github.com/DavidZemon/JumpstartedSkeleton. As the git log explains, there are eight basic steps:

  • Create the basic skeleton.
  • Add unit test support with Google Test (both GTest and GMock)
  • Add logic to CMake to optionally run the tests through valgrind
  • Add support for generating code coverage reports with gcov
  • Add custom CMake targets for executing clang-tidy and Cppcheck
  • Add support for generating an appropriate SonarQube properties file
  • Add a Conan recipe file and adjust CMake to utilize it when possible
  • Add a Make target to build Doxygen documentation

The JumpstartedSkeleton project was generated by the jumpstart Docker image – an interactive program that tailors its output to your needs. Understanding the contents of this article will provide a deeper understanding into how projects work when generated in this fashion.

The JumpstartedSkeleton project is released to the public domain under the Zero-Clause BSD license. You are free to do anything you wish with it, no attribution necessary (just don’t sue me).

Basic Project Structure

The project starts out with all of the basic CMake logic and C++ code organized into sensible directories. Understanding how to configure CMake and CPack as shown in the first commit requires prerequisite knowledge of the CMake build system and is therefore beyond the scope of this article, but specific questions are always welcome in the comments. (Enough feedback may prompt a revival of Learn CMake.)

A few key points about the project are worth pointing out before continuing on:

  • It contains a C++ library for consumers of the package
  • It contains a C++ executable, utilizing the above library
  • It contains an init script, intended for auto-starting the above-mentioned executable in Linux environments
  • It builds distributable packages in ZIP, DEB, and RPM formats. Windows and Mac package formats can be added easily enough with other CPack generators.

The folder structure is reasonably self-explanatory, but a common point of confusion is where to place headers:

  • Public headers (those intended to be consumed by users of the package) belong in the /include directory (relative to the project’s root). This entire directory will be included in distributable packages.
  • Private headers (those that should not be used by dependent code outside the project) belong in the same directory as the source files which use them, under /src. Headers under /src will not be included in distributable packages.

It is expected that large projects may need to create more subdirectories under /src to better organize each module. This is an exercise left up to the reader.

Google Test (GTest & GMock)

Commit number two brings in the single largest addition to the project: support for Google Test and its included mocking framework. I’ve grown accustomed to my tests residing in a separate directory from my sources, therefore all test-related code resides in the /tests directory. Similar to above, it may be advantageous to create further subdirectories to organize larger projects.

There are some extra features added in this commit – beyond the minimum requirement to link against libgtest.a and include its headers:

  • A test-all target is added as a workaround for the fact that ctest is not tied into the CMake build system and therefore will not build or rebuild test targets when source files change
  • An environment variable CI_GTEST_OUTPUT_DIR is checked and, if defined, used for all GTest output files. This allows CI servers to easily define where test reports should be placed for easy parsing and display in their own interfaces.
  • A simple CMake function, create_test is defined as a wrapper around CMake’s own add_test which makes it easy to take advantage of the above two features.

NOTE: The CMake enable_tests() command is executed from the project’s root for a friendlier developer experience than were it to be executed from the /tests directory. The ctest command can only be invoked in the build directory corresponding to where enable_tests() was invoked and any of its children (recursively). If the enable_tests() command is invoked underneath /tests, then invoking ctest at the project’s build root will fail with

david@balrog:~/Skeleton/cmake-build-debug$ ctest
*********************************
No test configuration file found!
*********************************
Usage

  ctest [options]

Changing into the tests subdirectory would yield successfully executed tests in this case

david@balrog:~/Skeleton/cmake-build-debug/tests$ ctest
Test project /home/david/reusable/Programming/CXX/Skeleton/cmake-build-debug/tests
    Start 1: JumpstartedSkeletonTest
1/2 Test #1: JumpstartedSkeletonTest ..........   Passed    0.00 sec
    Start 2: JumpstartedSkeleton-cliTest
2/2 Test #2: JumpstartedSkeleton-cliTest ......   Passed    0.00 sec

100% tests passed, 0 tests failed out of 2

Total Test time (real) =   0.01 sec

Valgrind

Adding support for valgrind is not a monumental task. The new TEST_WITH_VALGRIND option in the root CMake script allows enabling and disabling use of valgrind when tests are executed via ctest. The default value is OFF so as not to impede developer performance except when explicitly desired. CI servers should set this value to ON to help ensure memory leaks do not make it into production. Developers may want to temporarily enable this variable prior to submitting a code review.

Code Coverage (gcov)

Newer versions of GCC and LLVM make enabling code coverage quite simple: add the --coverage compiler flag and you’re off to the races. I find it advisable to only support code coverage in debug builds, so the flag is only defined in the CMAKE_C_FLAGS_DEBUG and CMAKE_CXX_FLAGS_DEBUG variables.

Another new CMake option, HTML_COVERAGE_REPORT, allows easy switching between HTML and XML output formats. The default value, ON, cause the reporting tool to format results in HTML instead of XML and is intended for use by developers. It is expected that CI servers will set the value to OFF for better consumption by tools like SonarQube.

Two new targets are added for generating the reports. The test-coverage target is generally more useful, as it will first invoke the test-all target and then regenerate coverage reports. For instances when tests may take a long time to execute, or if it is simply undesirable to re-build or re-run the tests, then report-coverage can be used to generate a coverage report without rebuilding or rerunning the tests.

Static Code Analysis (clang-tidy & Cppcheck)

One of the shortest commits in this project adds support for common static code analysis tools.

For Cppcheck, a custom target is added which will run cppcheck on-demand. This is not executed automatically as part of any other build targets.

CMake ships with explicit support for invoking clang-tidy immediately after the compiler for every source file. The feature is enabled by installing clang-tidy and then setting the CMAKE_<LANG>_CLANG_TIDY variables appropriately. It’s benefit is immediately visible by running make:

david@balrog:~/Skeleton/cmake-build-debug$ make
[ 10%] Building CXX object src/CMakeFiles/jumpstartedskeleton-lib.dir/JumpstartedSkeleton.cpp.o
/home/david/reusable/Programming/CXX/Skeleton/src/JumpstartedSkeleton.cpp:18:26: warning: method 'add' can be made static [readability-convert-member-functions-to-static]
int JumpstartedSkeleton::add(const int lhs, const int rhs) const {
                         ^                                 ~~~~~~

A clang-tidy configuration file, .clang-tidy is provided as a starting point. Like all static analysis tools, it is highly recommend that you and your team customize this as needed.

SonarQube

In an effort to remain as DRY and flexible as possible, the SonarQube properties file is generated at CMake configuration time, rather than hand-written by the project maintainer or jumpstart. CMake’s configure_file() function is leveraged to generate a properties file that perfectly matches the CMake project’s settings – it’s name, version number, source directories, and many different report paths.

One note worth making: the assignment of the CMake variable GTEST_OUTPUT_DIR is moved as part of this commit out of the /tests/CMakeLists.txt script and into the root CMakeLists.txt so that it is available for use when SonarQube’s properties file is generated.

Conan

The skeleton project is now almost complete. It has support for a myriad of tools, but there is one groundbreaking step left to take: the dependency manager.

With only the smallest of changes to the CMake scripts – a single line to the root CMake script – a Conan recipe can be dropped in place and used or not used quickly and easily. Use of Conan in this fashion allows for easy adoption by wary developers not yet ready to jump in with both feet. By using Conan’s cmake_find_package generator, this project keeps compatibility with both the system-provided dependencies and Conan-provided dependencies with ease. When Conan is available and in use, CMake will search first for dependencies provided by Conan. If Conan is not available or has not been executed yet, CMake will fall back to searching the system. Developers anxious to “try the hot new thing” can use Conan. CI servers and release managers can continue building “the old way” and, when everyone is comfortable that the project is building satisfactorily with Conan, a graceful migration consists merely of a change to the team’s build system documentation and CI server configurations.

Similar to SonarQube, DRY is an important principle for the Conan recipe. Because the version number for a project is constantly changing and required by both the Conan recipe and the CMake scripts, it is defined in a standalone text file to be read in by each tool as necessary. This ensures that the Conan package version number remains synchronized with any packages built by CPack.

Doxygen

Finally, one extra dependency is added which makes generating Doxygen documentation a breeze. CMake v3.9 introduced a high-level CMake function in the FindDoxygen.cmake module that avoids the boilerplate Doxyfile by providing sane defaults while allowing each of its values to be overridden with CMake variables. Adding Doxygen as a Conan dependency means developers have one less tool that needs to be installed system-wide.

Conclusion

The JumpstartedSkeleton project is an enterprise-ready example of a C++ project utilizing a suite of modern development tools. CMake is a crucial and central point of the entire system while Conan comes to the rescue as the most revolutionary.

Don’t forget to give the jumpstart Docker image a shot and generate your own C or C++ project. It’s easy-to-use interactive prompts will help you generate a skeleton project that fits your project requirements.

And don’t forget to comment below if this helped you in any way, or if you believe there is another tool that should be added to jumpstart!

About the Author

Object Partners profile.
Leave a Reply

Your email address will not be published.

Related Blog Posts
Natively Compiled Java on Google App Engine
Google App Engine is a platform-as-a-service product that is marketed as a way to get your applications into the cloud without necessarily knowing all of the infrastructure bits and pieces to do so. Google App […]
Building Better Data Visualization Experiences: Part 2 of 2
If you don't have a Ph.D. in data science, the raw data might be difficult to comprehend. This is where data visualization comes in.
Unleashing Feature Flags onto Kafka Consumers
Feature flags are a tool to strategically enable or disable functionality at runtime. They are often used to drive different user experiences but can also be useful in real-time data systems. In this post, we’ll […]
A security model for developers
Software security is more important than ever, but developing secure applications is more confusing than ever. TLS, mTLS, RBAC, SAML, OAUTH, OWASP, GDPR, SASL, RSA, JWT, cookie, attack vector, DDoS, firewall, VPN, security groups, exploit, […]