C++ developers have it great now, but we can forget how different it used to be not that long ago. One of the biggest issues in the community of C++ programmers is a lack of standardization, which meant a lot of different tools and processes were used to achieve the same thing, but often with different results and varying degrees of complexity and headaches. Having a standard means there’s a clear-cut “way of doing things.” However, if the standard is controversial (to say the least), is standardization a good thing?
This is the conversation that has swirled around CMake for some time, and opinions are predictably polarized on the topic. It has led some quoting horror writer H.P. Lovecraft to describe their experiences using CMake (as in this great tutorial) and reduced others to posting memes like this:
and, well, you get the picture. CMake is a source of frustration to some in the C++ community, to say the least. Even so, it remains a critical part of the C++ experience, so let’s dive in a little deeper into just why CMake is so common, and why it remains a controversial topic among developers.
Before we begin, some history
Back in the olden days, most projects that used libraries or executables that involved compiling several files used Make. Most Unix systems had some version of Make bundled with them, thus making it easy to create Makefiles, define their starting targets, and let Make handle the rest.
It became a matter of muscle memory for programmers given a source and a Makefile to simply type:
make && make install
Success or failure was easy to work with, and because the system was made for and by programmers, it worked (mostly) flawlessly.
Where problems started was when programmers wanted to compile the same source file across multiple systems. To do so effectively, the Makefile had to be individually configured. Resourceful programmers simply applied the fundamental theorem of software engineering to the problem and added a new level of indirection to the problem with Makefile.in.
This new layer meant that the configure script was the one creating a Makefile specific to each system. Now programmers just had to type in:
./configure && make && make install
The problem? Indirection breeds complexity, which is not always a good thing. What happens when your configure scripts are larger than the program you’re actually writing? For some, the solution was to add MORE indirection. This “answer” created a new problem – when indirection becomes too much indirection. The newfound complexity meant that even compiling short programs a nightmare and creating proper build systems became a pain. Thus, CMake seemed like a welcome solution.
Diving into the CMake debate
CMake was originally designed by Kitware as a build system generator that can produce Makefiles for Unix, Visual Studio, and XCode environments. It achieves this thanks to its single CMakeLists.txt file.
Using CMake breaks down the creation of a project or library into two steps. The first is using the CMakeLists.txt to create standard build files. Next, the platform’s native toolchain is used to actually build the project. Without getting too bogged down in the finer details, this means CMake essentially creates Make as a build system within the larger system and avoids the baggage that comes with fully relying on Make. This keeps projects clean of traditional build artifacts and reduces code pollution from object files and other similar relics. That all sounds great, right? So why is CMake still controversial?
Perhaps the biggest benefit CMake offers is its permissive BSD license, which lets anyone adopt it for build systems while avoiding vendor locking. It also means companies like Microsoft can still bundle it with their flagship IDEs. Another benefit of its open-source nature is that CMake has been under continuous development for the past 20 years, and over that time it has added support for tons of other languages in addition to C++.
CMake is also cross-platform. No matter what OS you’re working on (Linux, MacOS, Windows), you don’t need any specific configurations. You can skip the Makefiles, Visual Studio project settings, and batch files. CMake handles everything automatically.
You can also perform basic file manipulation using CMake. When you do need to interact with your filesystem, CMake offers a wide range of file commands that let you quickly and effectively deal with everything from reading to copying and transferring files.
Another key element (and a major improvement over previous tools like Make) is that CMake no longer requires you to drop into the command shell to operate it. Instead, newer versions include a GUI that is available for all supported platforms. This allows you to do things like configure your build before generating it, and even use CI/CD tools like Jenkins and Azure pipelines or even git workflows. This makes CMake adoption more likely and makes it much easier to use in a broader dev stack.
One other CMake benefit we’ll mention here is the ability to use caching to reduce parsing time at runtime. The platform creates a CMakeCache.txt file that is read whenever you re-run a project and reduces the loading time. However, this is also a perfect example of some of the reasons some devs are less than excited about CMake. So now, let’s look at the other side.
The less good
Let’s start with the caching example we just mentioned. For most beginners, this caching mechanism creates a problem. When you pass a variable via the command line, it’s stored in the cache. Whenever you access that variable in future runs with CMake, what you’ll get is the value stored within the cache, while the system ignores the new variable you pass through the command line. To solve this issue, you’ll have to make it explicitly undefined:
cmake -U <previously defined variable> -D <previously defined variable>[=new value]
Obviously, this isn’t a major issue, but it does highlight some of the inconveniences many devs have when it comes to CMake.
Another common complaint (which is subjective, as it pertains to preference), is that the syntax CMake uses is sometimes unintuitive. In some cases, the issue is that conditional statements might look more like function invocations;
if(condition) … else() … endif()
There are similar complaints across community boards and subreddits about syntax, with some complaining that even simple things (such as how set (x, “a:b:c”) should be a single string but is actually a list of strings). This, to be fair, is mostly a symptom of there not being a “standard” way of doing things and having to understand which specific method works best.
In terms of usability, there are some complaints about backward compatibility between CMake versions. Namely, the issues center on the fact that because of the way it’s been built, CMake includes a lot of parts and artifacts that don’t necessarily need to be there and could negatively impact a project file. This includes standard (if erroneous) methods that could cause programs to break but can’t be removed because of the prominence of CMake.
Closely related to this is the steep learning curve tied to using CMake. Because of the way the language has grown and expanded, there are a lot of accumulated scripts that, while useful in highly specific situations, tend to impact clean code and make it difficult to integrate with larger projects.
There’s no real answer to whether you should or shouldn’t use CMake. Keep in mind that the above is not a comprehensive list of either the good or the bad qualities the language has. In fact, CMake has become popular for a good reason, and for all the complaints, it’s still one of the most widely used tools by C++ devs out there.
The reality is that you’ll need to dive in and learn more about it (and its history) to understand if it’s the right tool for your project. In a lot of cases, you’ll find that it works great – especially in situations where you own the project end to end, which makes it easier to control the environment and manage dependencies.
However, you’ll need to be careful and understand what you do (and don’t) need and ensure that you’re not incorporating bad practices and “bad code” into your existing CMake files to avoid issues. You can also use excellent tutorials like the ones we’ve linked above to help you navigate your first CMake project. In either case, it’s worth diving in deeper and understanding just how it works, and its strengths, to make the most out of your CMake build.