I am implementing my own analogue of std::expected for C++17, and I am writing a method that takes a function and invokes it on the successful value of the expected. If the expected contains error, nothing happens. The method will return the expected itself.
The idea is that the given function should not return anything, e.g. logging. I want to generate a compile-time warning if the function returns non-void. How can I do that?
Below is the code of the method
#define NOT_INVOCABLE_MESSAGE "function must be invocable on the given type"
template <class Success, class Error>
class Expected {
public:
template<typename F>
Expected<Success, Error>& on_success(F &&f) & noexcept {
static_assert(is_invocable_v<F, Success &>, NOT_INVOCABLE_MESSAGE);
// IDE highlight on the first comma: "Expected end of line in preprocessor expression"
// compiler error: "error: operator '&' has no right operand"
#if !is_same_v<void, invoke_result_t<F, Success &>>
#warning "return value of the given function will be ignored"
#endif
// warning gets generated even when condition is false
if constexpr (!is_same_v<void, invoke_result_t<F, Success &>>) {
#warning "return value of the given function will be ignored"
}
if (_hasValue) std::forward<F>(f)(_success);
return *this;
}
private:
union {
Success _success;
Error _error;
};
bool _hasValue;
}
I try to generate warning using #warning
directive. I try two different conditionals: one with preprocessor #if
directive, and another one with if constexpr
. But the first one doesn't allow commas in the condition body, and the second one does not prevents #warning
from generating when condition is false. Are there any other ways?
You give the pre-processor too much credit. It has no concept of void
(or "return" or "type" or "function"). It knows that void
is a token, but anything deeper than that is for the compilation phase to process.
There is no standard way to accomplish what you want, but if you're willing to "cheat" a bit and assume warnings will be enabled (and not treated as errors), you could get close to what you want. A common warning is for an unused variable. With some carefully chosen names and data (and comments, if you want), you could convey your message to the programmer.
For example,
constexpr bool result_is_void = std::is_void_v<std::invoke_result_t<F, Success &>>;
if constexpr (!result_is_void) {
using Warning = std::enable_if_t<!result_is_void, const char *>;
Warning message = "return value of the given function will be ignored";
}
has no effect if invoking F
returns void
, but otherwise gcc will complain:
prog.cc: In instantiation of 'Expected<Success, Error>& Expected<Success, Error>::on_success(F&&) & [with F = int (&)(int); Success = int; Error = char]':
prog.cc:37:17: required from here
37 | e.on_success(fun);
| ~~~~~~~~~~~~^~~~~
prog.cc:17:17: warning: unused variable 'message' [-Wunused-variable]
17 | Warning message = "return value of the given function will be ignored";
| ^~~~~~~
Message received?
If you're concerned that people will read only the compiler's message and ignore the line on which it occurred, you could shove your message into the variable name, as in
constexpr bool result_is_void = std::is_void_v<std::invoke_result_t<F, Success &>>;
if constexpr (!result_is_void) {
using Warning = std::enable_if_t<!result_is_void, char>;
Warning return_value_of_the_given_function_will_be_ignored;
}
For which, gcc says (when invoking F
does not return void
):
prog.cc: In instantiation of 'Expected<Success, Error>& Expected<Success, Error>::on_success(F&&) & [with F = int (&)(int); Success = int; Error = char]':
prog.cc:37:17: required from here
37 | e.on_success(fun);
| ~~~~~~~~~~~~^~~~~
prog.cc:17:17: warning: unused variable 'return_value_of_the_given_function_will_be_ignored' [-Wunused-variable]
17 | Warning return_value_of_the_given_function_will_be_ignored;
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
One nice aspect of this variant is that the gcc underlined your entire message. The downside is that the available characters are limited, so you have to do some adjustments to your message, like replacing spaces with underscores (plus, it is a hack).
Why std::enable_if_t
?
Since this function template is nested inside another template, a discarded branch can trigger warnings (and does so with clang) when the outer template is processed. Making the Warning
type dependent on F
suppresses our goal warning until F
is known (when the inner template is instantiated), at which point a discarded branch is not instantiated (will not produce warnings).
As a footnote, I agree with the comment that in your particular case, you should probably not try to warn the user. It is very common for the return value of a function to be "eaten". In fact, your own code demonstrates that ignoring the return value is the default behavior, because the compiler does not warn when you invoke F
and ignore the result.
However, the question is larger than your motivating example, and an answer might be useful in other scenarios.