-Werror is Not Your Friend

I want to make a few points up front to short-circuit common misunderstandings with this article:

  1. I have a zero warning policy on my projects.
  2. I turn on a lot of warnings. -Wall, -Wextra, and a whole host of specific warning flags.
  3. I supplement the compiler warnings with analysis from a suite of static analysis tools, including cppcheck, clang-tidy, and clang scan-build.
  4. Zero-warning enforcement happens on the build server. Pull requests will be rejected and builds will be marked as failures if warnings are present.

I have to state all of this because many people think that my dislike of -Werror means that I am lax on warnings. Many also interpret it as a lack of professionalism. That’s not the case.

So, then, what’s wrong with -Werror?

-Werror is a compiler flag that causes all compiler warnings to be treated as errors. Developers who enable -Werror are making a statement: we care about our code base, and we won’t accept warnings here. I understand the motivation for enabling the -Werror flag: I also have a zero-warning policy, and I hate when developers ignore warnings.

My opinion on -Werror is based on my experiences as an open-source maintainer, a consumer of other people’s code, and a consultant who is constantly jumping into new proprietary code bases. I almost always find myself frustrated by the presence of -Werror. Most commonly, I check out the project, attempt to build it, and see that it fails to compile because there is a warning. Why is my build failing if I made no changes and your code base allows zero warnings?

-Werror Introduces a Toolchain Version Dependency

The reason: -Werror creates a project dependency on specific toolchain vendors and versions.

Different vendors have different warning sets and warning detection logic, even if they support a common array of warning settings (such as with GCC and Clang). Code that compiles with one toolchain warning-free may not do so with another toolchain. We often see this with our open-source projects. We primarily use Clang, and it is a common occurrence that our CI server will report a warning when compiling our “warning-free” code with GCC.

Whenever a new compiler version is released, new warnings are added (this is great!), detection for existing warnings is improved (also great!), and experimental warnings may be promoted. Less commonly, new versions include other changes that can impact warning generation. One example that comes to mind is when GCC 5.1.0 changed the default C language standard from gnu90 to gnu11. Some warnings are only enabled when targeting C99 or later, so anyone relying on the default (which we don’t recommend) would have seen new warnings after updating.

Sometimes warnings are not even consistently generated with the same toolchain vendor and version. One example that comes to mind is -Wunknown-pragmas. I am in the habit of using #pragma mark to provide nicer IDE interactions (e.g., sectioning code and adding “jump-to” points). Clang accepts this pragma without any problem. GCC will generate a warning when this is used. But the real problem is that gcc will recognize #pragma mark on MacOS, so you won’t see the warning until you build on another OS.

Note
I mean the official GCC build for MacOS, not the wrapper that ships with the Command Line Tools. This behavior is even documented in the GCC manual#pragma mark is only supported for Darwin targets.

In other cases, hard-coded build flags themselves are the source of trouble. Someone using an older compiler may find that a particular flag is not supported in a newer version. In many cases, this generates a warning (e.g., -Wunknown-warning). With -Werror, this build will fail for the user.

Note
Many build systems provide capabilities for checking whether given compiler flags are supported. We recommend using these approaches over hard-coding flags.

This toolchain dependency must be handled in one of a few ways:

  1. Documenting the supported toolchain(s)
  2. Capturing the build environment (whether in a container, virtual environment, cached packages, etc.)
  3. Ongoing maintenance to ensure that warnings are addressed for new compiler versions

Yet, the most common experience we have is that the dependency is not documented and a captured build environment is not provided. With open-source projects, this can lead to a build failure. Within companies, a more common scenario is that a new hire follows the setup guide but can’t get the project to compile. Only after talking to the team do they learn that a specific compiler version has to be used to build the project.

Enforce Zero Warnings at the DevOps Level

I have zero-warning policy (and I try to get my clients to adopt one as well). This policy is enforced by our CI server. To ensure we have a decent level of coverage, we build our software with a minimum of Clang, GCC, and at least one cross-compiler.

Another benefit of the DevOps enforcement approach is that you can expand warning coverage beyond just compiler warnings. For example, we also include warnings from static analysis tools in our zero-warning policy. We can also use our CI tools to generate warnings when FIXME and TODO comments are found in code.

Regardless of the source, if warnings are detected, the server will block pull requests and fail builds. This all happens without requiring -Werror.

This is a pragmatic solution: we have a zero-warning policy, have it enforced automatically, but still enable someone to successfully compile our projects if they are using a newer/different toolchain that reveals new warnings.

Certainly, -Werror can be used as part of the DevOps enforcement method. The only downside to using -Werror as the enforcement mechanism is that you will not be able to get a complete picture of the warnings present in a given build. Allowing compilation to succeed enables you to generate a complete report for the developer.

Refined Warning-As-Error Control

Even without the blanket use of -Werror, there are still a number of warnings that should always trigger an error. You can do this by specifying Werror=warning-name, which will cause that specific warning to generate an error.

For example, a warning that I promote to an error is -Wreturn-type.

Missing return statement in function with return expected
aws.c:158:1: warning: control reaches end of non-void function [-Wreturn-type]

If your function should return a value but does not actually do so, your function is going to be returning unexpected data, leading to weird behavior. This type of warning is definitely worth promoting to an error in any case!

Make -Werror Optional

When -Werror is used, I like to see it done as a configurable build option. This option can be used by the build server to enforce zero warnings. It can also be disabled if necessary, such as when a user can’t get a project to compile out-of-the-box due to a warning, or when you want to see the full set of warnings while migrating to a new toolchain version.

$ make all WARNINGS_AS_ERRORS=n

Some build systems, Meson, provide built-in options for this purpose. These should be preferred manually adding the -Werror flag in your build rules.

Summary

My objective was to communicate the more subtle implications of using -Werror. Newer compiler versions are likely to generate new warnings, making the build fail.

If you’re going to use this flag, make sure you clearly note the version(s) your project is expected to work with (or completely capture your build environment). Ideally, the use of -Werror will be a configurable option in your build system, regardless of whether or not is enabled by default.

You can achieve zero-warning policies through other means, and I prefer to do so with our DevOps pipeline.

Of course, everything has its tradeoffs. You might decide that -Werror really is the right tool for your team. Keep on, but take a moment to document or capture the build dependencies if you haven’t done so already.

Further reading
I have captured my answers to common points of contention in this post

14 Replies to “-Werror is Not Your Friend”

  1. I’ve been bitten by compiler-dependent -Werror behaviour. Nowadays the place to put this is in your continuous integration, not your Makefile. And, use multiple compilers on each OS and machine architecture you package for. Current and last-major-revision of Clang and GCC, for example. The idea is, warnings are bad, but warnings that only some users will see are worse.

    1. You raise great points. I’ve been reflecting on this after a few recent projects with clients who had 200+ warnings… in such a situation -Werror may have actually been their friend.

      You also bring to light a practice that I have but don’t think about – building for multiple compilers. Strangely I do this on my host machine but not on my current build server. I will fix that today.

  2. It is a tradeoff. Ignoring warnings may be convenient for the person who runs the build. On the other hand, new versions of compilers tend to optimize more aggressively, and may warn about undefined behaviour that was harmless before, but can cause hard-to-diagnose problems with the new compiler version. The fact that some undefined behaviour did not cause problems before, does not mean it won’t be a problem with a newer compiler. The -Werror setting has the advantage that somebody needs to address the warning, one way or another, instead of letting it pass and leaving it to the end users to deal with the consequences.

  3. I think a compromise is to insert -Werror between alpha and beta. And I’m not too sympathetic to the new hire who introduces warnings and doesn’t remove them before checking code is too lazy to understand C++ or to take an opportunity to go deeper.

  4. The projects I work on must be structured differently, because Warnings-as-errors works fine for us.
    It sounds like you’re assuming open-source, but many C++ projects are not open-source. Our customers don’t get source code, and wouldn’t have the foggiest if they did.
    It sounds like you support a fairly wide range of toolchain versions – that would be tough in more ways than one. We have the luxury of enforcing narrow toolchain restrictions.
    Most important perhaps: -Werror is automated and enforced by our CI, which for the projects I’ve worked on over the last few years, is preferable to having it fall on the shoulders of one frail, evanescent human being, no offense intended. What if you get seriously ill – and it lasts for months? What if your management (or board, or whoever) decides your personal oversight of warnings “isn’t the best use of your time”? What if you leave?
    When I joined my previous employer, the flagship product had accumulated thousands of warnings. This meant that nobody looked at ANY warning message, EVER. They were a startup, they’d had some engineering management churn, nobody made warnings a priority. When a skilled software manager came onboard, it was too late. Eliminating a warning usually meant touching working code and doing code review. Several years of “let’s try to eliminate 50 warnings during this sprint” basically didn’t move the needle at all.
    My conclusion, for the kind of projects I tend to work on namely closed-source projects in startup-y companies: If you can possibly turn on Werror, do it NOW, and lock it down.
    YMMV.

  5. I think it’s important to distinguish “hardcoding -Werror into your build system” vs “enabling -Werror on your CI with its known set of compilers.” The former can actualy cause errors if a user tries to build with an unusual compiler (e.g. PGI, Intel). The latter ensures that no warnings creep in under certain known conditions.

    1. Sure, I’d be fine with that case. I just rarely encounter it in my work. Usually it’s hard coded as a build flag.

      My personal preference is still to have my CI system fail the build if there is a non-zero count of warnings. Seems popular to think that I am allowing warnings if I don’t use Werror, but that is not what is written.

  6. You lost me in the third paragraph, with “because inevitably my first clean build of the project fails due to a spectacular mess of warnings.” We must have different definitions of “clean build.” :^)

    I’m retired now, so feel free to ignore this as “legacy”, but my experience has been that ignoring warnings has caused more problems, and more difficult-to-debug problems, than any nuisance factor arising from use of -Werror. If you’re seeing a ton of warnings that did not occur in the original development environment (since the project compiled with -Werror) then it seems very likely that the source code makes assumptions that may not be true in your environment.

    So, -Werror may not be your friend, but using -Wall -Wextra -Werror (or /W4 /we on MS compilers) has definitely been helpful for me.

  7. Hi Husoski,

    This article not about turning off or ignoring warnings,. I use -Wall, -Wextra, and many other specific warning flags, and a suite of static analysis tools. As mentioned int the article, I have a zero warning policy and push people toward that – I just think that should be enforced by the CI server and not the build scripts.

    “because inevitably my first clean build of the project fails due to a spectacular mess of warnings.” We must have different definitions of “clean build.” :^)

    I consider a “clean build” to be a build from a blank slate, as in, I have no build artifacts. That’s been the general usage I’ve encountered, though other interpretations seem reasonable.

    This scenario has repeated for me many times with both open source projects, as well as when working with clients on their projects when they have not clearly documented the compiler version they are using.

    If you’re seeing a ton of warnings that did not occur in the original development environment (since the project compiled with -Werror) then it seems very likely that the source code makes assumptions that may not be true in your environment.

    As the article points out, the most basic scenario is much simpler: as compilers improve, they a) make improvements to detecting problematic scenarios related to existing warnings, and b) add new warnings. So code that compiled warning-free with GCC 4.9 is likely not going to compile warning free with GCC 12 when you’re enabling more than the most basic set of warnings. This is the case for my own projects – I maintain a zero-warning policy, but every time I upgrade to a new major compiler version, new warnings are bound to surface in one project or another.

  8. I see the point where -Werror on all warnings is an overkill. And you talk a little bit on using specific -Werror=warning for potentially dangerous situations.

    As of now, we use these
    WARNINGS_AS_ERRORS := -Werror=undef -Werror=implicit-function-declaration -Werror=comment -Werror=int-conversion -Werror=stack-usage=2048

    Running program with any of these as warnings in most cases might be useless as you probably will run into undefined behavior or 100% bug.

    The first two are remnants of legacy projects when developers at this organization didn’t give a damn about what exactly did they build, whith which version of compiler and where did linker find all the functions and libraries. Turning these two on breaks each of old projects here. It’s a nightmare and I don’t allow anything like this happen anymore.

    -Werror=comment is an interesting one. We once had autogenerated sorce code in the project which put some comments on each line and one of the comments had backslash in it which turned off the next line of actual code. It was a fun bug. Zero warnings policy helped us find it quickly (we had exactly one warning after build adn it was ‘comment’). After that I decided to treat all such warnings as errors.

    stack_usage with reasonable amount (I see what I get with -fstack-usage and set accordingly) helps with interns and juniors when they recklessly declare huge arrays inside the function. It also helps to see when we get to the edge of stack size declared in linker script.

    Thanks for sharing your list of desired warnings. I’ll go over it and probably will start to use some of them too.

  9. In programs that I write and compile my current approach is this one:
    -Wall \
    -Wextra \
    -Wpedantic \
    -Wconversion \
    -Woverflow \
    -Werror \
    -Wno-error=unused-parameter \
    -Wno-error=unused-variable \
    -Wno-error=unused-but-set-variable \

    In other words: you can enable Werror but also selectively disable this by combining it with multiple Wno-error= flags as well.
    This should at least have been mentioned in this article.

    It always kind of surprises me how many flags we need to enable with GCC to get feedback about a lot of very serious kinds of problems in code. And many of those should definitely be considered errors by other programming languages than C and C++.

  10. But the real problem is that gcc will recognize #pragma mark on MacOS, so you won’t see the warning until you > build on another OS.

    On macOS the call to ‘gcc’ is just a wrapper to clang.

Share Your Thoughts

This site uses Akismet to reduce spam. Learn how your comment data is processed.