Today I would like to dive into the topic of unused parameters in C and C++: why they may happen and how to properly deal with them—because smart compilers will warn you about their presence should you enable -Wunused-parameter
or -Wextra
, and even error out if you are brave enough to use -Werror
.
Why may unused parameters appear?
You would think that unused parameters should never exist: if the parameter is not necessary as an input, it should not be there in the first place! That’s a pretty good argument, but it does not hold when polymorphism enters the picture: if you want to have different implementations of a single API, such API will have to provide, on input, a superset of all the data required by all the possible implementations.
The obvious case of the above is having an abstract method implemented by more than one subclass (which you can think of as a function pointer within a struct
in the case of C). In this scenario, the caller of this abstract method may be handling a generic condition but the various specific implementations may or may not use all the input data.
Our example
An example taken straight from Kyua is the compute_result
method, whose purpose is to determine the status of a test case after termination based on the outputs of the test program, including: the program’s exit code, its standard output, its standard error, and files that may be left in the transient work directory. The signature of this abstract method looks like this:
virtual model::test_result compute_result(
const optional< process::status >& status,
const fs::path& work_directory,
const fs::path& stdout_path,
const fs::path& stderr_path) const = 0;
Kyua implements this interface three times: one for plain test programs, one for ATF-based test programs, and one for TAP-compliant test programs. This interface receives all test-related post-termination data as inputs so that the different implementations can examine any parts (possibly not all) they require to compute the result.
In concrete terms: the plain interface only looks at the exit status; the ATF interface looks both at the exit status and at a file that is left in the work directory; and the TAP interface looks both at the exit status and the standard output of the program.
When you face an scenario like this where you have a generic method, it is clear that your code will end up with functions that receive some parameters that they do not need to use. This is alright. However, as obvious as it may be to you, the compiler does not know that and therefore assumes a coding error, warning you along the way. Not helpful.
Two simple but unsuitable alternatives
A first mechanism around this, which only works in C++, is to omit the parameter name in the function definition. Unfortunately, doing so means you cannot reference the parameter by name any longer in your documentation and, furthermore, this solution does not work for C.
A second mechanism is to introduce side-effect free statements in your code of the form (void)unused_argument_name;
. Doing this is extremely ugly (for starters, you have to remember to keep such statements in sync with reality) and I fear is not guaranteed to silence the compiler—because, well, the compiler will spot a spurious statement and could warn about it as well.
Because these two solutions are suboptimal, I am not going to invest any more time on them. Fortunately, there is a third alternative.
Tagging unused parameters with compiler attributes
The third and best mechanism around this is to explicitly tag the unused parameters with the __attribute__((unused))
GCC extension as follows:
model::test_result compute_result(
const optional< process::status >& status,
const fs::path& work_directory __attribute__((unused)),
const fs::path& stdout_path __attribute__((unused)),
const fs::path& stderr_path __attribute__((unused))) const;
But this, as shown, is not portable. How can you make it so?
Making the code portable
If you want your code to work portably across compilers, then you have to go a bit further because the __attribute__
decorators are not standard. The most basic abstraction macro you’d think of is as follows:
#define UTILS_UNUSED __attribute__((unused))
… which you could parameterize as:
#define UTILS_UNUSED @ATTRIBUTE_UNUSED@
… so that your configure.ac
script could determine what the right mechanism to mark a value as unused in your platform is and perform the replacement. This is not trivial, so take a look at Kyua’s compiler-features.m4 for configure.ac
to get some ideas.
Such a simple macro then lets you write:
model::test_result compute_result(
const optional< process::status >& status,
const fs::path& work_directory UTILS_UNUSED,
const fs::path& stdout_path UTILS_UNUSED,
const fs::path& stderr_path UTILS_UNUSED) const;
… which gets us most of the way there, but not fully.
Going further
The UTILS_UNUSED
macro shown above lets the compiler know that the argument may be unused and that this is acceptable. Unfortunately, if an argument is marked as unused but it is actually used, the compiler will not tell you about it. Such a thing can happen once you modify the code months down the road and forget to modify the function signature. If this happens, it is a recipe for obscure issues, if only because you will confuse other programmers when they read the code and cannot really understand the intent behind the attribute declaration.
My trick to fix this, which I’ve been using successfully for various years, is to define a macro that also wraps the argument name; say: UTILS_UNUSED_PARAM(stdout_path)
. This macro does two things: first, it abstracts the definition of the attribute so that configure
may strip it out if the attribute is not supported by the underlying compiler; and, second and more importantly, it renames the given argument by prefixing it with the unused_
string. This renaming is where the beauty lies: the name change will forbid you from using the parameter via its given name and thus, whenever you have to start using the parameter, you will very well know to remove the macro from the function definition. Has worked every single time since!
Here is how the macro looks like (straight from Kyua’s defs.hpp.in file):
#define UTILS_UNUSED @ATTRIBUTE_UNUSED@
#define UTILS_UNUSED_PARAM(name) unused_ ## name UTILS_UNUSED
And here is how the macro would be used in our example above:
/// This is a Doxygen-style docstring.
///
/// Note how, in this comment, we must refer to our unused
/// parameters via their modified name. This also spills to our
/// public API documentation, making it crystal-clear to the
/// reader that these parameters are not used. Because we are
/// documenting here a specific implementation of the API and not
/// its abstract signature, it is reasonable to tell such details
/// to the user.
///
/// param status Status of the exit process.
/// param unused_work_directory An unused parameter!
/// param unused_stdout_path Another unused parameter!
/// param unused_stderr_path Yet another unused parameter!
///
/// return The computed test result.
model::test_result compute_result(
const optional< process::status >& status,
const fs::path& UTILS_UNUSED_PARAM(work_directory),
const fs::path& UTILS_UNUSED_PARAM(stdout_path),
const fs::path& UTILS_UNUSED_PARAM(stderr_path)) const;
What about Doxygen?
As I just mentioned Doxygen above, there is one extra trick to get our macros working during the documentation extraction phase. Because Doxygen does not implement a full-blown C/C++ parser—although I wish it did, and nowadays this is relatively easy thanks to LLVM!—you have to tell Doxygen how to interpret the macro. Do so with the following code to the Doxyfile
control file:
PREDEFINED = "UTILS_UNUSED="
PREDEFINED += "UTILS_UNUSED_PARAM(name)=unused_ ## name"
So, what about you? Do you keep your code warning-free by applying similar techniques?