Search code examples
c++clangcompiler-warningssuppress-warnings

Clang issues -Wunused-value depending on whether the code is called from a macro


I use a special assertion macros called CHECK. It is implemented like this:

#define CHECK(condition)  check(condition).ok ? std::cerr : std::cerr

The user can choose to provide additional information that is printed if the assertion fails:

CHECK(a.ok());
CHECK(a.ok()) << a.to_string();

Notice the ternary operator in macro definition. It ensures that a.to_string() is executed only when the assertion fails. So far so good. I've been using this (and other similar) macros for a long time without any problems.

But recently I found that clang issues “expression result unused [-Wunused-value]” warning regarding the second std::cerr if CHECK is used inside another macro:

#define DO(code)  do { code } while(0)

int main() {
    do { CHECK(2 * 2 == 4); } while(0);  // no warning
    DO( CHECK(2 * 2 == 4); );  // warning
}

Full example: https://godbolt.org/z/5bfnEGqsn.

This makes no sense to me. Why would this diagnostic depend on whether the code was expanded from a macro or not? GCC issues no warnings in either case.

Two questions:

  • Is there any reason for such behavior or should I file this as clang bug?
  • How can I suppress this without disabling “-Wunused-value” altogether? I've tried [[maybe_unused]] and __attribute__((unused)) but they don't seem to work on statements.

Solution

  • I don't think that this is a good solution what I suggest here but you could change your code so that you will always use your std::cerr, by changing your check(condition).ok ? std::cerr : std::cerr to check(condition).ok ? std::cerr << "" : std::cerr << "":

    #include <iostream>
    
    
    struct CheckResult {
        CheckResult(bool ok_arg) : ok(ok_arg) {}
        ~CheckResult() { if (!ok) abort(); }
        bool ok;
    };
    
    inline CheckResult check(bool ok) {
        if (!ok) std::cerr << "Assertion failed!\n";
        return CheckResult(ok);
    }
    
    #define CHECK(condition)  \
        check(condition).ok ? std::cerr << "" : std::cerr << ""
    
    #define DO(code)  \
        do { code } while(0)
    
    
    int main() {
        do { CHECK(2 * 2 == 4); } while(0);
        DO( CHECK(2 * 2 == 4); );
    }
    

    Another thing you could do is to use a function that returns that std::cerr:

    #include <iostream>
    
    
    struct CheckResult {
        CheckResult(bool ok_arg) : ok(ok_arg) {}
        ~CheckResult() { if (!ok) abort(); }
        bool ok;
    };
    
    inline CheckResult check(bool ok) {
        if (!ok) std::cerr << "Assertion failed!\n";
        return CheckResult(ok);
    }
    
    [[maybe_unused]] inline std::ostream & get_ostream() {
        return std::cerr;
    }
    
    #define CHECK(condition)  \
        check(condition).ok ? get_ostream() : get_ostream()
    
    #define DO(code)  \
        do { code } while(0)
    
    
    int main() {
        do { CHECK(2 * 2 == 4); } while(0);
        DO( CHECK(2 * 2 == 4); );
    }
    

    The [[maybe_unused]] here is not about the returned value but about the function in case that you change your code so that it is not used under certain conditions (maybe not needed here).

    My major concern about your approach is this statement:

    Notice the ternary operator in macro definition. It ensures that a.to_string() is executed only when the assertion fails.

    Without reading the documentation and just looking at CHECK(a.ok()) << a.to_string(); on one would assume that a.to_string() will only be executed if the assertion fails.

    For a standpoint of code review or collaboration, this can be really problematic.