9

我有以下项目结构:

test_main.cc

#define CATCH_CONFIG_MAIN

#include "catch2.hpp"

测试1.cc

#include "catch2.hpp"
#include "test_utils.hpp"

TEST_CASE("test1", "[test1]") {
  REQUIRE(1 == 1);
}

测试2.cc

#include "catch2.hpp"
#include "test_utils.hpp"

TEST_CASE("test2", "[test2]") {
  REQUIRE(2 == 2);
}

test_utils.hpp

#pragma once
#include <iostream>

void something_great() {
  std::cout << ":)\n";
}

如果我使用类似的东西进行编译clang++ -std=c++17 test_main.cc test1.cc test2.cc,则该函数something_great在 test1.o 和 test2.o 中都定义了。这会导致错误,例如

duplicate symbol __Z15something_greatv in:
    test1.cc.o
    test2.cc.o
ld: 1 duplicate symbol for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

在 Catch2 文档的Scaling Up部分中,他们提到为了拆分您的测试,您可能需要

根据测试需要使用尽可能多的附加 cpp 文件(或任何你称之为实现文件的文件),但分区对你的工作方式最有意义。每个附加文件只需要 #include "catch.hpp"

但是在文档的示例部分中,我没有看到像我这样的用例。我阅读了这篇博文,其中描述了三种对我不感兴趣的解决方案:将函数定义为宏,或将函数staticinline.

是否有另一种方法来编译这些文件,这些文件产生一个可执行文件,其主要功能由定义test_main.cc

4

2 回答 2

7

这实际上与 Catch 或测试无关。当您使用 C++ 编写文件时,它会被逐字#include复制粘贴到该行。#include如果你把自由函数定义放在头文件中,你会在构建你的实际程序时看到这个问题,等等。

根本问题是#includeimport-a-module 指令与大多数语言中的等效指令(import,require等)不同,它们在这种情况下做的事情很正常(确认标头是相同的我们已经看到并忽略了重复的方法定义)。

建议您编写的评论inline者在技术上是正确的,因为这将“解决您的问题”,因为您的编译器不会多次为该方法生成目标代码。但是,它并没有真正解释正在发生的事情或解决根本问题。


干净的解决方案是:

  • test_utils.hpp中,将方法定义替换为方法声明void something_great();
  • test_utils.cc使用方法的定义(您当前在 中)创建.hpp
  • clang++ -std=c++17 test1.cc -c
  • clang++ -std=c++17 test2.cc -c
  • clang++ -std=c++17 test_main.cc -c
  • clang++ -std=c++17 test_utils.cc -c
  • clang++ -std=c++17 test1.o test2.o test_utils.o test_main.o

我还建议您阅读以下内容:定义和声明之间有什么区别?

明确地:

// test_utils.hpp
#pragma once

// This tells the compiler that when the final executable is linked,
// there will be a method named something_great which takes no arguments
// and returns void defined; the definition lives in test_utils.o in our
// case, although in practice the definition could live in any .o file
// in the final linking clang++ call.
void something_great();

和:

// test_utils.cpp
#include "test_utils.hpp"
#include <iostream>

// Generates a DEFINITION for something_great, which
// will get put in test_utils.o.
void something_great() { std::cout << "Hi\n"; }

每次对测试进行更改时,您似乎都担心“重新编译 Catch”。我不想告诉你,但你现在处于 C++ 领域:你将毫无意义地重新编译东西。当包含它们的源文件发生更改时,必须在某种程度上“重新编译”像 Catch 这样的仅头文件库,因为无论好坏,如果源文件或从源文件传递包含的头文件包含catch2.hpp,那么源代码catch2.hpp将读取该源文件时由编译器解析。

于 2019-03-15T01:01:24.347 回答
-1

经过一些实验,我找到了一个合理的解决方案,它不需要您在任何时候对测试进行更改时完全重新编译 Catch。

和之前一样定义test_main.cc :

#define CATCH_CONFIG_MAIN

#include "catch2.hpp"

添加另一个 .cc 文件test_root,其中包含您的测试文件作为标题:

#include "test1.hpp"
#include "test2.hpp"

将您的测试源更改为标题:

测试1.hpp

#pragma once
#include "catch2.hpp"
#include "test_utils.hpp"

TEST_CASE("test1", "[test1]") {
  REQUIRE(1 == 1);
}

测试2.hpp

#pragma once
#include "catch2.hpp"
#include "test_utils.hpp"

TEST_CASE("test2", "[test2]") {
  REQUIRE(2 == 2);
}

单独编译

clang++ -std=c++17 test_main.cc -c
clang++ -std=c++17 test_root.cc -c
clang++ test_main.o test_root.o

其中 test_main.cc 只需要编译一次。每当您更改测试时,都需要重新编译 test_root.cc,当然您必须重新链接两个目标文件。

如果有更好的解决方案,我暂时不接受这个答案。

于 2019-03-14T23:05:31.973 回答