Visual Studio 2019 and [[nodiscard]] in its Default C++14 Mode

“Back-porting” and enabling [[nodiscard]] in C++14 code bases is a feature that can come in handy.

[[nodiscard]] is a feature that was added in C++17. However, it’s interesting to note that the Visual C++ compiler that comes with Visual Studio 2019 accepts [[nodiscard]] also when you compile C++ code in the default C++14 mode.

C++ project property page in VS 2019, showing the default C++14 mode selected.
Visual Studio 2019 C++ Project Properties

If, for some reason, you can’t compile your C++ code base with C++17 mode enabled, I would still suggest to use [[nodiscard]] in your C++ code, at least in places where it can help spotting subtle bugs (like in the cases suggested here).

C4834 warning message emitted by VC++ 2019 in its default C++14 mode, when the return value of a [[nodiscard]] function is discarded.
C4834 [[nodiscard]]-related warning message emitted by VC++ in default C++14 mode

You could even use some conditional compilation with preprocessor macros to enable [[nodiscard]] when compiling your C++ code with compilers like VC++ 2019 that support this feature, and expand that preprocessor macro to nothing in C++ compilers that don’t support it.

Where to Apply [[nodiscard]]?

A couple of suggestions for applying the [[nodiscard]] attribute in C++ code.

In the previous blog post, I shared some thoughts on [[nodiscard]]. As I wrote there, it would be better to have a good default of always assuming [[nodiscard]], and opt out from that good default only in some specific cases. But, unfortunately, the current state of the C++ language standard is different, and actually the opposite of the above.

So, where would I strongly recommend to apply [[nodiscard]]?

The Case of Bad Confusing Naming Choices

A common case that comes to my mind is the example of std::vector::empty. I have always found that vector’s method name confusing. I mean: Is “empty” a method to empty the vector, that is to remove its content? No. std::vector::empty is a bool-returning method to check if the vector has no element.

So, in this case, applying [[nodiscard]] to the vector::empty method will make the C++ compiler generate a warning when you write this kind of C++ code:

// myVector is a std::vector

// Empty the vector content [NOTE: Wrong code]
myVector.empty();

As can be read from the comment above, the intention of the programmer was to empty the vector. But the vector::empty method simply returned a bool to check if the vector had no elements. And, in the above case, that returned value was discarded.

So, the use of the [[nodiscard]] attribute with the vector::empty method will generate a warning in the above bogus case. It’s like the C++ compiler telling the programmer: “Hey, programmer! You discarded the return value of vector::empty!” So the programmer thinks: “Hey, what return value? Wait a minute… I just wanted to empty the vector! Ahhh…Ok. I got it: This vector::empty method returns a bool and is used to check if the vector is empty. So, the method I should call is another one!” And then, after some search, the vector::clear method is invoked and the above code fixed.

// Correct code to empty the vector:
myVector.clear();

Note: The so maltreated MFC, with its CArray class, did choose a much better naming for its “empty” method, calling it IsEmpty. With such a good name, you won’t certainly make the mistake of invoking IsEmpty when you actually meant to make the vector empty. The designers of the STL made a confusing choice for the vector::empty method: calling it is_empty would have been much better and clear.

So, this is actually a case in which [[nodiscard]] comes to the rescue for a bad confusing choice for naming methods.

The Case of Returning Raw Owning Resource Handles

Another important case where I strongly suggest to apply the [[nodiscard]] attribute is when you return to the caller some raw owning handle or pointer (which you can think of as an “handle” to memory resources).

For example, in my WinReg C++ library, I wrap a raw Windows Registry HKEY handle in the safe boundaries of the RegKey C++ class.

However, for flexibility of design, I chose to have a RegKey::Detach method that transfers the ownership of that HKEY raw handle to the caller:

// Transfer ownership of current HKEY to the caller.
// Note that the caller is responsible for closing the key handle!
[[nodiscard]] HKEY Detach() noexcept;

After invoking that method, the caller becomes the new owner of the returned HKEY. As such, the caller will be responsible for properly tracking and releasing the raw handle when not needed anymore.

In such cases, discarding (by mistake) the returned value of the RegKey::Detach method would cause a resource leak. So, having the C++ compiler speak up in such cases would definitely help in preventing such leaks. So, if you have to be selective of where you apply the [[nodiscard]] attribute, this is certainly a great place for it!

To [[nodiscard]] or Not [[nodiscard]] by Default?

Some reflections on the use (or abuse?) of the [[nodiscard]] attribute introduced in C++17.

An interesting addition made in C++17 has been the [[nodiscard]] attribute. The basic idea is to apply it such that the C++ compiler will emit a warning when the return values of functions or methods are discarded.

Someone said that, even in previous versions of the C++ standard, if your C++ compiler didn’t emit a warning when you discard the return value of functions or methods, you should discard that C++ compiler.

Really?

Well, consider this simple piece of C++ code:

int GetSomething()
{
    return 10;
}

int main()
{
    GetSomething();
}

I tried compiling that code with VS 2019 at warning level 4 (which is a good warning level for VC++), and the code compiles successfully, with no warnings emitted by the VC++ compiler.

Of course, I won’t discard VC++, as I consider it the best C++ compiler for Windows (and Visual Studio is my favorite IDE for Windows).

On the other hand, if I add the [[nodiscard]] attribute to the GetSomething function:

[[nodiscard]] int GetSomething()
{
    ...

then the VC++ compiler does emit a warning message:

warning C4834: discarding return value of function with ‘nodiscard’ attribute

So, should C++ compilers emit warnings on discarded return values by default?

Well, if you think about it, printf returns an int, which I think a lot of programmers discard 🙂

So, if C++ compilers would emit such warnings by default, compiling lots of code that uses printf to print some text to the standard output would result in very noisy compilations.

Well, you may argue that printf comes from C. But I could reply that it’s used a lot in C++ (at least until the recent std::print and related formatting functions added in C++20/23).

Moreover, even focusing on more C++-specific features, even something like operator= overloads return a reference that is many times discarded:

T& operator=(const T& other)
{
    ...
    return *this;
}

Should an innocent assignment like:

a = b;

trigger a warning??

So, assuming a default [[nodiscard]] policy, that would indeed generate lots of noisy compilation sessions!

On the other hand, adding [[nodiscard]] to many functions and methods can result in very noisy and kind of cluttered code, too.

So, we need to find a balance here.

A good alternative could be adding [[nodiscard]] to structs or classes or enumerations that represent an error information or something that shouldn’t be discarded, so the C++ compiler will automatically apply the [[nodiscard]] behavior to functions and methods that return those [[nodiscard]] classes or enumerations. Note that this behavior is already implemented in C++17, and available in C++ compilers like Visual C++ 2019.

Another option I was thinking about is this: Assume [[nodiscard]] behavior as the default, and opt out using another attribute, like [[maybe_discarded]], in cases like the operator= overloads, in which discarding the returned value or reference is perfectly sensible. This would adhere to the philosophy of good defaults. In other words, if you don’t specify anything, you’ll get [[nodiscard]] behavior by default, and warning messages. And in those specific cases when discarding the returned value can make sense, only there you specify an explicit attribute like [[maybe_discarded]].