使用googletest的EXPECT_NO_THROW和std::array时出现编译错误

5

我在尝试使用googletest中的std::array时遇到了此错误。以下是一个产生此错误的最小示例:

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();
}

我使用了来自Github的 current googletest代码。 使用 make 和 make install 命令安装googletest。

作为编译器,我在Ubuntu 14.04 LTS上使用了clang3.8。

使用以下命令:

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

结果如下:

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.

移除 EXPECT_NO_THROW 宏并简单地声明数组可以编译通过。我是否遗漏了什么明显的东西,还是应该在 Github 上提交 bug 报告?


2
宏不理解C++,尤其是<,>模板和模板参数内的逗号。这些逗号被解析为宏的多个参数。首先,使用using uchar_16_array = std::array<unsigned char, 16>,并在宏中使用uchar_16_array。我假设它会理解初始化列表(但不能保证)。 - Yakk - Adam Nevraumont
2
你可以使用另一组括号来解决Yakk提到的问题:EXPECT_NO_THROW(( ... )); 这将把 ... 中的任何代码作为单个参数传递给宏。 - dyp
1
@dyp,不幸的是,多余的括号仍然被保留下来,因此如果宏没有处理它们,可能会破坏替换文本。 - chris
@chris 不是很确定我理解你的意思,但我意识到 gtest 在那个上下文中期望一个实际的语句(它以 ; 结尾),因此在括号中包含语句当然不会直接起作用。但是,你可以将语句包装在 lambda 中,并将其放入括号中传递:EXPECT_NO_THROW(( [&]{ my_code; }() )); - dyp
1
我强烈建议您在编译时启用警告: -Wall -Wextra 一开始可能会有些麻烦,但它们可以使编译器输出关于程序意外后果的重要信息。 - dyp
2个回答

10
EXPECT_NO_THROW 是一个宏定义,其定义如下:
#define EXPECT_NO_THROW(statement) \
  GTEST_TEST_NO_THROW_(statement, GTEST_NONFATAL_FAILURE_)

如您所见,这是一个类似函数的宏,需要一个参数。预处理器(处理宏的工具)处理的是令牌。它不理解C++或者C语言,只理解自己的令牌语言。(现在,编译和预处理好像是同时进行的,但我指的是预处理语言的语义。)
预处理器期望EXPECT_NO_THROW只有一个参数。对于函数式宏来说,它通过查找逗号来分离参数。因此,当它看到一个指定为函数式宏参数列表中的令牌列表时,就会将其视为单个参数。
EXPECT_NO_THROW( const std::array<unsigned char, 16> foo = {1, 2, 3}; )

然后,它将参数列表分解为以下参数:

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

当函数式宏 EXPECT_NO_THROW 需要一个参数时,这些当然是多个参数。


为了将包括 , 在内的多个预处理标记作为单个参数传递给函数式宏,您可以将这些标记括在括号中:

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

然而,这段代码无法编译:

EXPECT_NO_THROW 宏的展开如下:

#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.")

不可到达代码宏的定义如下:

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

因此,如果您将语句STMT放在EXPECT_NO_THROW宏内部,您将得到以下结果:

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

因此,如果您将(STMT;)放入EXPECT_NO_THROW中,您最终会得到一行代码。
if (::testing::internal::AlwaysTrue()) { (STMT;); };

(STMT;);这部分不是合法的C ++。如果STMT是作为OP中的声明,那么(STMT);也不是合法的。

如果你把({STMT;})传递到宏中,你最终会得到({STMT;});,这在C ++中仍然是不允许的,但是它被g ++作为扩展允许;它是一个表达式语句。在这里,{STMT;}部分被解释为一个表达式,用括号括起来形成表达式({STMT;})

你也可以尝试隔离逗号。正如Yakk在对OP的评论中指出的,你可以使用typedef将逗号隐藏在模板参数列表中;初始化器列表中剩余的逗号可以通过使用临时变量来包装,例如:

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

尽管原始的EXPECT_NO_THROW(STMT)允许STMT是一个语句,但在C++中,语句不能任意地用括号括起来。然而,表达式可以任意地用括号括起来,并且表达式可以用作语句。这就是为什么将语句作为表达式语句传递就可以解决问题的原因。如果我们可以将数组声明构造为一个表达式,那么这个问题就会得到解决:

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

请注意,这将创建一个临时数组;这不是像原始问题中的声明语句,而是一个单独的表达式。

但是,创建一个我们想要测试的内容的表达式可能并不总是这么简单。然而,在标准C++中有一个可以包含语句的表达式:lambda表达式。

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

请注意在lambda表达式后面的(),这是执行lambda语句的重要部分!忘记加上括号是一个非常微妙的错误来源 :(

1
一旦我们获得了source_location,我们就可以通过将lambda传递给函数模板来摆脱这个宏混乱。这也消除了显式手动的() 调用lambda的需要。 - dyp

2
许多人在评论中指出宏和模板不太兼容。这个限制是否在googletest文档中有记录?我找不到任何暗示这种限制的东西。
如果您有更多解决方法,请提供给我,我会将它们添加到我的答案中。如果您有真正的答案,请分享 :)
接下来,我尝试了所提议的解决方案,并使用以下方式进行编译:
clang++ -std=c++11 -o arr arr.cpp -lgtest_main -lgtest -lpthread

额外加上 EXPECT_NO_THROW 参数的大括号确实可以起作用:
#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();
}

提供使用声明不起作用:
#include "gtest/gtest.h"
#include <array>

using uchar_16_arr = std::array<unsigned char, 16>;

TEST(Test, Positive) {
    EXPECT_NO_THROW({
        const uchar_16_arr foo = {1, 2, 3};
    });
}

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

由于仍然导致相同的错误消息,因此:
arr.cpp:8:38: error: too many arguments provided to function-like macro invocation
        const uchar_16_arr foo = {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:7:5: note: cannot use initializer list at the beginning of a macro argument
    EXPECT_NO_THROW({
    ^               ~
arr.cpp:7:5: error: use of undeclared identifier 'EXPECT_NO_THROW'
    EXPECT_NO_THROW({
    ^
2 errors generated.

我认为using声明不起作用的原因是{在预处理器中没有特殊含义,因此直到第一个,之前的所有内容都被视为第一个宏参数。另一方面,()在预处理器中具有特殊含义(特别是在宏替换中):宏的参数分离寻找除括号内以外的, - dyp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接