Conditional compile-time warning in C++

436 views Asked by At

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?

2

There are 2 answers

3
JaMiT On BEST ANSWER

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.

0
user17732522 On

#warning is a preprocessor directive. The preprocessor runs before actual C++ compilation. It has no concept of C++ types, expressions, statements or scopes. The preprocessor considers the source code to be a sequence of preprocessor tokens and preprocessor directives only.

The #warning is conditional only on the #if and #include directives, which in turn don't know about C++ constructs. They only support textual inclusion, macro-based token substitution and simple arithmetic on integers.

There is no generic C++-level function that tells the compiler to emit a warning, but still forces it to compile the program. There is static_assert, which will however cause compilation to fail instead of just warn.

You can potentially abuse the [[deprecated]] attribute to cause a warning at compile-time in the actual compilation phase. See Does there exist a static_warning?