Blog 2024 06 05 The limits of `[[maybe_unused]]`
Post
Cancel

The limits of `[[maybe_unused]]`

The codebase I work with defines a utility macro called UNUSED. Its implementation is straightforward, it takes a variable (let’s call it x) as a parameter and expands it as ((void)x). The reason we use it is to avoid compiler warnings for unused variables.

Why don’t we delete those variables you might ask? We usually end up with the need for UNUSED when we use preprocessor macros to include certain pieces of code only in debug builds, for example, debug logs. This is a simple enough example, right?

1
2
3
4
5
auto result = doSomething(param);
#ifdef DEBUG
    std::cout << "Result: " << result << '\n';
#endif
UNUSED(result);

In this case, if we compile in release mode, result is only used by UNUSED. If we haven’t had that, the compilation would fail in release mode.

But we don’t like macros, do we? They are error-prone due to their limited readability and complicated debugging.

Can we do something better?

First of all, even though I agree with Arne and I dislike macros, I think that the macro is still more readable in this case than what it hides: (void)x;

But since C++17 we also have the [[maybe_unused]] attribute at our hands. If an entity is declared with this label, any lack of usage emitted warning will be suppressed.

Can it replace our UNUSED macro?

The answer is sadly no.

It’s true that [[maybe_unused]] can be used in a lot of places.

Starting from C++26 it can even mark attributes as potentially unused ones. Until then, we have the following possibilities.

Any class / struct or union can be declared as such: class [[maybe_unused]] Wrapper. Though I’ve barely seen a compiler complaining that a class is unused…

typedefs or alias declarations using the using keyword can also be declared with [[maybe_unused]].

1
2
using Squad [[maybe_unused]] = std::vector<Player>;
[[maybe_unused]] typedef std::vector<Player> Squad;

Local and non-static data members can also be [[maybe_unused]], just like functions, enumerators and enumerations. [[maybe_unused]] can even be used with structured bindings.

1
2
3
4
5
6
7
8
9
10
enum [[maybe_unused]] E {
	A [[maybe_unused]],
	B [[maybe_unused]] = 42
};


[[maybe_unused]] void foo([[maybe_unused]] int param) {
	[[maybe_unused]] bar = 3 * param;
	assert(bar); // only compiled in debug mode
}

Though, with structured bindings, we are reaching the limits of [[maybe_unused]]. If you use [[maybe_unused]], then all the subobjects are declared as maybe unused. You cannot simply mark specific subobjects. It’s one or nothing.

1
2
3
// both 'a' and 'b' might be unused
// you cannot have only one of them [[maybe_unused]]
[[maybe_unused]] auto [a, b] = std::make_pair(42, 0.23);

So why did I say that it cannot replace the UNUSED macro?

Well, there is one thing it cannot mark maybe unused. Lambda captures.

If you have a lambda capture that will be only part of the debug build, the release build will complain. And you have no way to use [[maybe_unused]] with a lambda capture. When this question came up at a mailing list, the answer of a committee member was that you should use (void)x;, as it means less clutter and it’s easier to read and maintain. Quite ironic as this solution can be always used, yet [[maybe_unused]] seems superior in terms of readability.

1
2
3
4
5
6
7
auto foo = doSomething(param);
auto callback = [&foo] () {
	#ifdef DEBUG
	    std::cout << "foo: " << foo << '\n';
	#endif
    UNUSED(result); // or (void)result;
};

Too bad.

What can we do?

We can keep using our good old UNUSED macro.

Conclusion

In this article, we’ve seen that the [[maybe_unused]] label can help us suppress compiler warnings for variables (and other entities) that are only used in certain builds. Sadly, it doesn’t work in all situations, you cannot use it with lambda captures. In those situations, we still need other solutions, such as plain cast to void or a macro.

Connect deeper

If you liked this article, please

This post is licensed under CC BY 4.0 by the author.