Avoiding unused variables warnings when using assert() in a Release build

前端 未结 16 1685
隐瞒了意图╮
隐瞒了意图╮ 2020-12-05 09:10

Sometimes a local variable is used for the sole purpose of checking it in an assert(), like so -

int Result = Func();
assert( Result == 1 );
<
相关标签:
16条回答
  • 2020-12-05 09:33

    This is a bad use of assert, IMHO. Assert is not meant as an error reporting tool, it's meant to assert preconditions. If Result is not used elsewhere, it's not a precondition.

    0 讨论(0)
  • 2020-12-05 09:37

    With C++17 we can do:

    [[maybe_unused]] int Result = Func();
    

    though it involves a bit of extra typing compared to a assert substitution. See this answer.

    Note: Added this because is the first google hit for "c++ assert unused variable".

    0 讨论(0)
  • 2020-12-05 09:42

    Most answers suggest using static_cast<void>(expression) trick in Release builds to suppress the warning, but this is actually suboptimal if your intention is to make checks truly Debug-only. The goals of an assertion macro in question are:

    1. Perform checks in Debug mode
    2. Do nothing in Release mode
    3. Emit no warnings in all cases

    The problem is that void-cast approach fails to reach the second goal. While there is no warning, the expression that you've passed to your assertion macro will still be evaluated. If you, for example, just do a variable check, that is probably not a big deal. But what if you call some function in your assertion check like ASSERT(fetchSomeData() == data); (which is very common in my experience)? The fetchSomeData() function will still be called. It may be fast and simple or it may be not.

    What you really need is not only warning suppression but perhaps more importantly - non-evaluation of the debug-only check expression. This can be achieved with a simple trick that I took from a specialized Assert library:

    void myAssertion(bool checkSuccessful)
    {
       if (!checkSuccessful)
        {
          // debug break, log or what not
        }
    }
    
    #define DONT_EVALUATE(expression)                                    \
       {                                                                 \
          true ? static_cast<void>(0) : static_cast<void>((expression)); \
       }
    
    #ifdef DEBUG
    #  define ASSERT(expression) myAssertion((expression))
    #else
    #  define ASSERT(expression) DONT_EVALUATE((expression))
    #endif // DEBUG
    
    int main()
    {
      int a = 0;
      ASSERT(a == 1);
      ASSERT(performAHeavyVerification());
    
      return 0;
    }
    

    All the magic is in the DONT_EVALUATE macro. It is obvious that at least logically the evaluation of your expression is never needed inside of it. To strengthen that, the C++ standard guarantees that only one of the branches of conditional operator will be evaluated. Here is the quote:

    5.16 Conditional operator [expr.cond]

    logical-or-expression ? expression : assignment-expression

    Conditional expressions group right-to-left. The first expression is contextually converted to bool. It is evaluated and if it is true, the result of the conditional expression is the value of the second expression, otherwise that of the third expression. Only one of these expressions is evaluated.

    I have tested this approach in GCC 4.9.0, clang 3.8.0, VS2013 Update 4, VS2015 Update 4 with the most harsh warning levels. In all cases there are no warnings and the checking expression is never evaluated in Release build (in fact the whole thing is completely optimized away). Bare in mind though that with this approach you will get in trouble really fast if you put expressions that have side effects inside the assertion macro, though this is a very bad practice in the first place.

    Also, I would expect that static analyzers may warn about "result of an expression is always constant" (or something like that) with this approach. I've tested for this with clang, VS2013, VS2015 static analysis tools and got no warnings of that kind.

    0 讨论(0)
  • 2020-12-05 09:44

    You could create another macro that allows you to avoid using a temporary variable:

    #ifndef NDEBUG
    #define Verify(x) assert(x)
    #else
    #define Verify(x) ((void)(x))
    #endif
    
    // asserts that Func()==1 in debug mode, or calls Func() and ignores return
    // value in release mode (any braindead compiler can optimize away the comparison
    // whose result isn't used, and the cast to void suppresses the warning)
    Verify(Func() == 1);
    
    0 讨论(0)
  • 2020-12-05 09:44
    int Result = Func();
    assert( Result == 1 );
    

    This situation means that in release mode, you really want:

    Func();
    

    But Func is non-void, i.e. it returns a result, i.e. it is a query.

    Presumably, besides returning a result, Func modifies something (otherwise, why bother calling it and not using its result?), i.e. it is a command.

    By the command-query separation principle (1), Func shouldn't be a command and a query at the same time. In other words, queries shouldn't have side effects, and the "result" of commands should be represented by the available queries on the object's state.

    Cloth c;
    c.Wash(); // Wash is void
    assert(c.IsClean());
    

    Is better than

    Cloth c;
    bool is_clean = c.Wash(); // Wash returns a bool
    assert(is_clean);
    

    The former doesn't give you any warning of your kind, the latter does.

    So, in short, my answer is: don't write code like this :)

    Update (1): You asked for references about the Command-Query Separation Principle. Wikipedia is rather informative. I read about this design technique in Object Oriented Software Construction, 2nd Editon by Bertrand Meyer.

    Update (2): j_random_hacker comments "OTOH, every "command" function f() that previously returned a value must now set some variable last_call_to_f_succeeded or similar". This is only true for functions that don't promise anything in their contract, i.e. functions that might "succeed" or not, or a similar concept. With Design by Contract, a relevant number of functions will have postconditions, so after "Empty()" the object will be "IsEmpty()", and after "Encode()" the message string will be "IsEncoded()", with no need to check. In the same way, and somewhat symetrically, you don't call a special function "IsXFeasible()" before each and every call to a procedure "X()"; because you usually know by design that you're fulfilling X's preconditions at the point of your call.

    0 讨论(0)
  • 2020-12-05 09:44
    // Value is always computed.  We also call assert(value) if assertions are
    // enabled.  Value is discarded either way.  You do not get a warning either
    // way.  This is useful when (a) a function has a side effect (b) the function
    // returns true on success, and (c) failure seems unlikely, but we still want
    // to check sometimes.
    template < class T >
    void assertTrue(T const &value)
    {
      assert(value);
    }
    
    template < class T >
    void assertFalse(T const &value)
    { 
      assert(!value);
    }
    
    0 讨论(0)
提交回复
热议问题