Modern Development Environment for C++: Part 1
Please allow me to introduce you to a new world, full of bright, new (and old) tools which, when combined, make writing and developing C/C++ fun again.
These tools should cover all aspects of your local development environment. Shared resources, like CI/CD, are out of scope for this article, but may be covered in a future article (hint: Docker). Part 2 of this article will begin to walk through what a project might look like that combines all of these tools. At the end of this series, you will have all of the information you need to support new and update old C/C++ projects geared towards the modern developer.
For now, let’s talk about what this tech stack might look like, and why these tools were chosen.
The tools and tool spaces covered in this article are:
- Dependency management: Conan
- Build system: CMake
- Testing Framework: Google Test
- Mocking Library: Google Mock
- Code coverage: gcov
- Static code analysis: clang-tidy, Cppcheck
- Dynamic code analysis: valgrind
- Editor: JetBrains CLion
- Debugger Frontend: JetBrains CLion
- Debugger Backend: gdb
Dependency Management: Conan
The C/C++ world has long relied on system package managers (
.rpm, among others) and manually installed packages for any libraries used by a given codebase. This is, at best, a step that is defined and controlled outside of the given codebase, and at worst it is a digression point where one developer’s machine yields a different build result from another’s.
In searching for an appropriate dependency management solution, I had a few requirements:
- Must allow two versions of the same project to be used on a given machine at the same time (aka: OpenSSL v1.0.0 and OpenSSL V1.1.0)
This is the deal-breaker that causes almost all system package managers to be insufficient for development
- Must be build system agnostic
No one wants to re-write the build system for OpenSSL just to use OpenSSL as a dependency, so the package manager must be designed with the expectation of bundling the output of various build systems. This, unfortunately, rules out Gradle, which would have otherwise been a great solution.
- Must be cross-platform – both host and target
The package manager must run on a variety of OSes and CPU architectures, and it must be able to support cross-compiled packages.
These requirements alone do not narrow the playing field down to Conan. Diving into all of the reasons why Conan was selected is a longer discussion than belongs in this article, but suffice it to say that after using Conan for approximately two years, my team was very happy. Conan is also managed by JFrog, the same people behind Artifactory and Xray, so we can rest assured it won’t disappear as quickly as Google Wave.
Build System: CMake
I fully admit that my choice of build system is a case of the cart leading the horse. When I read that JetBrains was developing an IDE for C++, and that that IDE would only support the CMake build system, my only reaction was “Well, I guess it’s time to learn CMake!”
That being said, CMake has proven itself to be a fantastic build system for C and C++ projects. Its custom scripting language leaves something to be desired, but it is syntactically simple and doesn’t take long to learn. Have a look at my Learn CMake course for a quick start.
CMake supports some key features that are crucial to modern build systems for C/C++:
- Capable of cross-compiling
- Extendable toolchain support (able to work with one-off embedded toolchains)
- Create multiple artifacts in a single project
- Store meta-data on build targets (what include folders will be required when linking, what compiler flags, etc)
- Utilize transitive dependencies
- Create visual dependency graph (CMake actually creates dot files, which Graphviz then turns into pictures)
- Create importable “packages” for other CMake projects to use when importing as a dependency
It is feasible that other, newer build systems may be a better choice today. It is worth digging into Gradle and/or Buck if your team or organization is just spinning up in the C/C++ world or has not yet converted to a modern build system (for instance, if your team is still using GNU Make, IMake, GNU AutoTools, etc).
Testing Framework: Google Test
Every language needs a decent unit testing framework, and Google Test (GTest) serves the purpose well. It supplies many standard assertions, legible error messages when an assertion fails, and auto-registered tests. The basics like test suites, setup, and teardown methods are also all included. It isn’t the smallest framework in the world – if you’re running on a limited device like an 8-bit micocontroller, you may not be able to fit the static library in your ROM.
In general, I don’t need much out of my unit testing framework. GTest gets the job done and its philosophy (names for assertions, methods, etc) is a close match with JUnit, so it was a quick learn for this Java guy.
Mocking Library: Google Mock
What would a testing framework be without a mocking framework to sit alongside it? GMock is a far stretch from the simplicity that is Mockito, but it’s about as good as it gets without an interpreter running behind the scenes. GMock allows you to create mock classes that inherit from your dependencies. Assertions can be set on the mocks to ensure that methods are called with appropriate parameters and all of the standard criteria that you may have used elsewhere.
GMock is also included as part of the “Google Test” package. It is a separate library file, but the same source repository. I recommend them both because they are both well designed and easy to use, but it is possible to mix and match with other libraries (for instance, one could use Catch2 for the test framework combined with GMock).
Code Coverage: gcov
Code coverage is a bit harder to implement in the C++ world than Java, but the good folks at GNU have provided us with gcov – one of the many tools in the GNU Compiler Collection (GCC). Gcov can export its data in both HTML and XML, for consumption either by a human in a browser or by SonarQube. Along with that, CLion has native support for displaying code coverage results from gcov.
Clang’s llvm-cov would also work nicely if your project utilizes LLVM instead of GCC.
Static Code Analysis: clang-tidy, Cppcheck
Compilers in the C++ world contain many more warnings than Java developers may be used to, so it’s worth starting with a robust set of compiler options, such as
-Werror -Wall -Wpedantic -Wconversion. However, clang-tidy and Cppcheck add an extra set of rules that will feel much more familiar to Java developers with their SonarQube servers. Like any static code analysis tool, it may take a long time to tweak the ruleset to your liking, but the combination of clang-tidy and Cppcheck can help ensure bugs and bad smells in your code are squashed as quickly as possible.
Dynamic Code Analysis: valgrind
Valgrind is best known for catching memory leaks. It has some other uses, but they pale in comparison. Another thing Valgrind is well known for: cryptic output. CLion comes to the rescue, however, and decodes its output on your behalf, making it easy to navigate to the exact file and line where memory was leaked.
The largest drawback to this tool is its lack of cross-platform support. I have not researched Windows alternatives, though it may be that WSL/WSL2 is sufficient enough at this point.
Editor: JetBrains CLion
Vim, Emacs, and even Eclipse had their days. Today is the day of JetBrains. Their IDEs are second to none, and if you haven’t tried them, I encourage you to do so. In 2014, JetBrains began development of a dedicated C/C++ IDE which eventually adopted the name CLion. Today, it is a top-notch IDE that provides most of the functionality that Java (and Android), Python, and PHP developers have come to expect from the likes of IntelliJ IDEA, Android Studio, PyCharm, and PhpStorm.
Eclipse can do many of the same things that CLion supports, but its CMake and VCS support can not compare to CLion. Alternatively, if your projects’ build systems are not supported by CLion (and converting them to a build system that is supported by CLion is not an option) then Eclipse may be your next best bet.
Microsoft’s Visual Studio Code – a relatively new player in this field – is another editor worthy of your investigation. Through extensive use of third-party plugins, VS Code can do many of things that more mature and heavier IDEs bundle as part of the initial installation.
Debugger Frontend: JetBrains CLion
Like any modern IDE, CLion provides a beautiful frontend for the debugger. This is a graphical interface that allows setting breakpoints and stepping through the code. Users of JetBrain’s other products will be immediately familiar with its interface and happy to see that more advanced features – such as conditional breakpoints – are still available.
To make life even easier, CLion bundles a recent version of GDB and LLDB with the installation, though your system-provided or manually-installed versions may also be selected for use.
Debugger Backend: GDB
The GNU debugger, GDB, is generally considered the de facto standard for debugging on Linux and a variety of other platforms. It does the job and it does it well. Its standard interface is an interactive CLI, but various IDEs are capable of connecting to it through a TCP port, which allows those IDEs to provide their own, more intuitive, UI.
LLVM’s lldb (bundled with CLion) can be easily swapped with GDB if your project builds on top of other LLVM tools instead of GCC.
GDB provides easy access to remote-debugging features, crucial for developers that write code on Windows and execute that code on Linux, or are targeting embedded Linux devices separate from their workstations.
There is a key tool that I believe is missing from this stack: mutation testing. I have not yet found a suitable mutation testing tool that fits in this stack. I’d like to invite you to comment below if you have used a mutation tool for a C/C++ project and whether or not you believe it could fit well with a CMake-based project.
With this selection of tools, it becomes feasible to enjoy the comfort of a modern development environment even when developing against legacy code and niche products. My team and I successfully used this stack to develop embedded Linux projects from both Windows and Linux hosts, and I hope this has piqued your interest in learning or adopting a new tool for your own efforts – even if it isn’t one of the specific options listed here.
In the next article, I will begin to dive into the technical details of how these tools merge together into a cohesive project and development experience.