Search code examples
c++c++11googleteststdarray

Compilation error using googletest's EXPECT_NO_THROW with std::array


I got this error while trying to work with std::array in googletest. Following is a minimal example that produces this error:

arr.cpp

#include "gtest/gtest.h"
#include <array>

TEST(Test, Positive) {
    EXPECT_NO_THROW({
        const std::array<unsigned char, 16> foo = {1, 2, 3};
    });
}

int main(int argc, char **argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

I used the current googletest code from github. To make & make install googletest.

As a compiler I used clang3.8 on Ubuntu 14.04 LTS.

Using the follownig command:

clang++ -std=c++11 -o arr arr.cpp

Results in:

arr.cpp:6:41: error: too many arguments provided to function-like macro invocation
        const std::array<unsigned char, 16> blasdasd = {1, 2, 3};
                                        ^
/usr/local/include/gtest/gtest.h:1845:9: note: macro 'EXPECT_NO_THROW' defined here
#define EXPECT_NO_THROW(statement) \
        ^
arr.cpp:5:5: note: cannot use initializer list at the beginning of a macro argument
    EXPECT_NO_THROW({
    ^               ~
arr.cpp:5:5: error: use of undeclared identifier 'EXPECT_NO_THROW'
    EXPECT_NO_THROW({
    ^
2 errors generated.

Removing the EXPECT_NO_THROW macro and simply declaring the array compiles fine. Is there anything obvious I am missing or should I file a bug report on github?


Solution

  • The EXPECT_NO_THROW is a macro defined as follows:

    #define EXPECT_NO_THROW(statement) \
      GTEST_TEST_NO_THROW_(statement, GTEST_NONFATAL_FAILURE_)
    

    As you can see, this is a function-like macro which takes one argument. The preprocessor (which deals with macros) works on tokens. It does not understand C++, nor C, but only its own token language. (Nowadays, compilation and preprocessing happen in one stage apparently, but I'm referring to the semantics of the preprocessor language.)

    The preprocessor expects a single argument for EXPECT_NO_THROW. It separates arguments for function-like macros by looking for commas. So when it sees a list of tokens in the argument list for a function-like macro such as:

    EXPECT_NO_THROW( const std::array<unsigned char, 16> foo = {1, 2, 3}; )
    

    then it separates the argument list into arguments as follows:

    • const std::array<unsigned char
    • 16> foo = {1
    • 2
    • 3};

    And these are of course multiple arguments where one is expected for the function-like macro EXPECT_NO_THROW.


    In order to pass several preprocessing tokens including , as a single argument to a function-like macro, you can enclose those tokens in parentheses:

    EXPECT_NO_THROW( (const std::array<unsigned char, 16> foo = {1, 2, 3};) );
    

    However, this will not compile:

    The EXPECT_NO_THROW macro is expanded as follows:

    #define GTEST_TEST_NO_THROW_(statement, fail) \
      GTEST_AMBIGUOUS_ELSE_BLOCKER_ \
      if (::testing::internal::AlwaysTrue()) { \
        try { \
          GTEST_SUPPRESS_UNREACHABLE_CODE_WARNING_BELOW_(statement); \
        } \
        catch (...) { \
          goto GTEST_CONCAT_TOKEN_(gtest_label_testnothrow_, __LINE__); \
        } \
      } else \
        GTEST_CONCAT_TOKEN_(gtest_label_testnothrow_, __LINE__): \
          fail("Expected: " #statement " doesn't throw an exception.\n" \
               "  Actual: it throws.")
    

    Where the unreachable code macro is defined as follows:

    #define GTEST_SUPPRESS_UNREACHABLE_CODE_WARNING_BELOW_(statement) \
      if (::testing::internal::AlwaysTrue()) { statement; }
    

    So, if you put a statement STMT inside the EXPECT_NO_THROW macro, you'll end up with:

      if (::testing::internal::AlwaysTrue()) {
        try {
          if (::testing::internal::AlwaysTrue()) { STMT; };
        }
      // ...
    

    Therefore, if you put (STMT;) into EXPECT_NO_THROW, you end up with a line

    if (::testing::internal::AlwaysTrue()) { (STMT;); };
    

    The part (STMT;); is not legal C++. Neither is (STMT); if that STMT is a declaration as in the OP.

    If you pass ({STMT;}) into the macro, you'll end up with ({STMT;}); which is still illegal in C++, but it's allowed in g++ as an extension; it's an expression-statement. Here, the {STMT;} part is interpreted as an expression, enclosed in parentheses to form the expression ({STMT;}).

    You can also try to isolate the commas. As Yakk pointed out in a comment to the OP, you can hide the comma in the template-argument list by using a typedef; the remaining commas in the initializer-list can be wrapped by using a temporary, for example:

    using array_t = std::array<unsigned char, 16>;
    EXPECT_NO_THROW( const array_t foo = (array_t{1, 2, 3}); );
    

    While the original EXPECT_NO_THROW(STMT) does allow STMT to be a statement, statements in C++ cannot be arbitrarily enclosed in parentheses. Expressions however can be arbitrarily enclosed in parentheses, and expressions can be used as a statement. This is why passing the statement as an expression-statement works. If we can formulate our array declaration as an expression, this will solve the problem:

    EXPECT_NO_THROW(( std::array<unsigned char, 16>{1, 2, 3} ));
    

    Notice this creates a temporary array; this is not a declaration-statement as in the OP, but a single expression.

    But it might not always be this simple to create an expression of the things we want to test. However, there's one expression in standard C++ which can contain statements: A lambda-expression.

    EXPECT_NO_THROW(( []{ const std::array<unsigned char, 16> foo = {1, 2, 3}; }() ));
    

    Please note the () after the lambda which is important to actually execute the statement within the lamdba! Forgetting this is a very subtle source of errors :(