每个 CTest 可执行文件有几 MB - 我如何使它们更小?

Each CTest executable is several MB - how do I make them smaller?

我有一堆单元测试想从 Visual Studio 的测试框架迁移到更容易访问的东西。我开始将一些移动到 ctest,其中(据我了解)每个测试都需要编译为单独的可执行文件。

我的 cmakelists.txt 看起来像:

cmake_minimum_required(VERSION 3.10)
project(UnitTests)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

set(CMAKE_MACOSX_RPATH 1)



include(CTest)
enable_testing()

add_executable(Test1 Test1.cpp)
add_executable(Test2 Test2.cpp)


add_test(First Test1)
add_test(Second Test2)

问题是 Test1 和 Test2 分别编译为 2.7 和 2.6 MB 的文件。这是因为每个人都必须有 #include "BigClass.h",其中 BigClass 是我真正想要测试的庞大而庞大的项目。

我可能有几十个测试要迁移,所以编译 运行 所有这些测试可能 运行 成数百兆字节的可执行文件,其中大部分 space 只是重复 BigClass!

中的所有代码

我试图将此 class 作为一个库包括在内:

add_library(BigClass SHARED ../path/to/BigClass.cpp)

然后在文件末尾:

target_link_libraries(Test1 PUBLIC BigClass)

但这并没有影响space。

我想我想要的是一种动态 link 到 BigClass 的方法(反正已经编译好了),所以所有的测试都可以共享代码。但我不确定如何执行此操作,也不太确定如何搜索它(例如,动态搜索 linked #include 表明这实际上是不可能的)。

无论如何都可以帮我找到:

  1. 一个避免所有这些问题的跨平台 C++ 测试框架?
  2. 一种在 ctest 中编译单元测试以共享大量相同代码的方法?
  3. 有关如何 link C++ class 动态执行的说明?
  1. a cross-platform C++ testing framework that avoids all these problems?

忽略代码建议 off-topic…
我们正在使用 catch2,对此我很满意。

  1. a way to compile unit tests in ctest to share a lot of the same code?

测试还有其他应用。所以,下一个问题的答案应该包括这个。

  1. directions on how to link C++ classes dynamically?

我制作了一篇 MCVE,试图涵盖整个故事。做好准备……


当代码被写入成为 DLL 的一部分时,它必须导出所有符号,这些符号将在其他 DLL 或可执行文件中可见。其他 DLL 和可执行文件必须依次导入这些符号。微软提供了这个主题的一般介绍:dllexport, dllimport.

有一个常用的宏技巧来确保可以在 DLL 内部(需要 dllexport)和 DLL 外部(需要 dllimport)使用相同的 headers需要)。

这在 BigClass.h 中得到了证明:

#ifndef BIG_CLASS_H
#define BIG_CLASS_H

/* A macro trickery to provide export and import prefixes
 * platform dependent for Windows and Linux.
 */
#ifdef _WIN32
// for Windows, Visual C++
#define DLL_EXPORT __declspec(dllexport)
#define DLL_IMPORT __declspec(dllimport)
#else // (not) _WIN32
// for gcc
#define DLL_EXPORT __attribute__((visibility("default")))
#define DLL_IMPORT __attribute__((visibility("default")))
#endif // _WIN32

/* The macro trickery to define how exported symbols shall
 * be included.
 */
#ifdef BIG_CLASS_STATIC
#define BIG_CLASS_API
#else // (not) BIG_CLASS_STATIC
#ifdef BUILD_BIG_CLASS
#define  BIG_CLASS_API DLL_EXPORT
#else // (not) BUILD_BIG_CLASS
#define BIG_CLASS_API DLL_IMPORT
#endif // BUILD_BIG_CLASS
#endif // BIG_CLASS_STATIC

// the library interface

#include <iostream>

struct BigClass {
  const int id = 0;

  // inline constructors doesn't need an export guard
  BigClass() = default;

  // non-inline constructors need an export guard
  BIG_CLASS_API BigClass(int id);

  // non-inline functins as well
  BIG_CLASS_API void print() const;

  // no export guard for inline member functions
  void printLn() const
  {
    print();
    std::cout << std::endl;
  }
};

// functions need to be exported
extern BIG_CLASS_API void print(const BigClass&);

// inline functions don't need to be exported
inline void printLn(const BigClass& big)
{
  big.printLn();
}

#endif // BIG_CLASS_H

从而引入第一个宏DLL_EXPORTDLL_IMPORT来隐藏Windows(Visual Studio)和Linux(g++ ).

这些定义可以在多个库中共享。

第二组宏是特定于库的——因此,所有宏中的前缀 BIG_CLASS_

BIG_CLASS_STATIC 是一个宏,当(且仅当)库被构建为静态库时,它应该被定义为编译器的参数。如果必须将静态库 link 编辑为可执行文件,也必须对其进行定义。

BUILD_BIG_CLASS 是一个宏,如果(且仅当)库被构建并且 link 编辑为 DLL(或共享 object 在 Linux).

这两个宏都被认为是准备第三个宏BIG_CLASS_API,扩展为

  • 没有静态库
  • DLL_EXPORT如果构建了DLL
  • DLL_IMPORT 如果构建了任何其他旨在使用和 link DLL 的内容。

对应BigClass.cc:

#include "BigClass.h"

BigClass::BigClass(int id): id(id) { }

void BigClass::print() const
{
  std::cout << "BigClass::print(): *this: BigClass { id: " << id << " }";
}

void print(const BigClass& big)
{
  std::cout << "print(): "; big.print();
}

没什么特别的。

BigClass.hBigClass.cc 将用于构建示例 BigClass.dll

此外,两个测试应用程序 Test1Test2 应 link 编辑 BigClass.dll

Test1.cc:

#include <iostream>

#include <BigClass.h>

#define TEST(...) std::cout << #__VA_ARGS__ << ";\n"; __VA_ARGS__ 

int main()
{
  std::cout
    << "Test1:\n"
    << "======\n";
  TEST(BigClass big);
  TEST(big.print());
  std::cout << '\n';
  TEST(big.printLn());
  TEST(BigClass big1(1));
  TEST(big1.print());
  std::cout << '\n';
  TEST(big1.printLn());
  std::cout << "Done." << std::endl;
}

Test2.cc:

#include <iostream>

#include <BigClass.h>

#define TEST(...) std::cout << #__VA_ARGS__ << ";\n"; __VA_ARGS__ 

int main()
{
  std::cout
    << "Test2:\n"
    << "======\n";
  TEST(BigClass big);
  TEST(print(big));
  std::cout << '\n';
  TEST(printLn(big));
  TEST(BigClass big2(2));
  TEST(print(big2));
  std::cout << '\n';
  TEST(printLn(big2));
  std::cout << "Done." << std::endl;
}

同样,它们没有什么特别之处。

免责声明:TEST()与单元测试无关。这只是一种演示摆弄。

最后,CMakeLists.txt 将所有内容放在一起(例如在 VS 解决方案中 BigClassDLL.sln):

project (BigClassDLL)

cmake_minimum_required(VERSION 3.10.0)

set_property(GLOBAL PROPERTY USE_FOLDERS ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
if (MSVC)
  # Unfortunately neither
  # CMAKE_CXX_STANDARD_REQUIRED ON
  # nor 
  # CMAKE_CXX_EXTENSIONS OFF
  # force the /permissive- option
  # which makes MSVC standard conform.
  add_compile_options(/permissive-)
endif()

# enable shared libraries (aka. DLLs)
option(BUILD_SHARED_LIBS "Build using shared libraries" ON)

include_directories("${CMAKE_SOURCE_DIR}")

# build the library
add_library(BigClass
  BigClass.cc BigClass.h)
set_target_properties(BigClass
  PROPERTIES
    PROJECT_LABEL "Big Class DLL"
    DEFINE_SYMBOL BUILD_BIG_CLASS)
if (NOT BUILD_SHARED_LIBS)
  target_compile_definitions(BigClass PUBLIC BIG_CLASS_STATIC)
endif()

# build the tests
add_executable(Test1 Test1.cc)
target_link_libraries(Test1 BigClass)

add_executable(Test2 Test2.cc)
target_link_libraries(Test2 BigClass)

关于动态 linking:

有一些具体的事情
  1. option(BUILD_SHARED_LIBS "Build using shared libraries" ON)
    这在 CMake: Step 9: Selecting Static or Shared Libraries.

    中有解释
  2. set_target_properties(BigClass PROPERTIES DEFINE_SYMBOL BUILD_BIG_CLASS)
    这确保宏 BUILD_BIG_CLASS 将在构建库 BigClass 时在编译器的命令行中专门定义。

  3. if (NOT BUILD_SHARED_LIBS)
      target_compile_definitions(BigClass PUBLIC BIG_CLASS_STATIC)
    endif()
    

    BIG_CLASS_STATIC 将在构建或使用库 BigClass 的每个编译器命令行中定义。

免责声明: 我知道 CMakeLists.txt 用于多个项目被认为是糟糕的风格。对于一个严肃的项目,库当然应该驻留在自己的目录中,并带有自己的 CMakeLists.txt 文件。我把它放在一起是为了尽可能地减少它(也是因为我可以)。


2019 年 Visual Studio 的情况:

2019 年 Visual Studio 的构建工件:

运行 Test1.exe:

Test1:
======
BigClass big;
big.print();
BigClass::print(): *this: BigClass { id: 0 }
big.printLn();
BigClass::print(): *this: BigClass { id: 0 }
BigClass big1(1);
big1.print();
BigClass::print(): *this: BigClass { id: 1 }
big1.printLn();
BigClass::print(): *this: BigClass { id: 1 }
Done.

运行 Test2.exe:

Test2:
======
BigClass big;
print(big);
print(): BigClass::print(): *this: BigClass { id: 0 }
printLn(big);
BigClass::print(): *this: BigClass { id: 0 }
BigClass big2(2);
print(big2);
print(): BigClass::print(): *this: BigClass { id: 2 }
printLn(big2);
BigClass::print(): *this: BigClass { id: 2 }
Done.

最后(为了解决可移植性问题),我在 Linux:

中构建了相同的 CMake 项目
Scheff'sCat@debian:/mnt/hgfs/hostD/Entwicklung/tests/C++/dll$ mkdir build-Linux
Scheff'sCat@debian:/mnt/hgfs/hostD/Entwicklung/tests/C++/dll$ cd build-Linux
Scheff'sCat@debian:/mnt/hgfs/hostD/Entwicklung/tests/C++/dll/build-Linux$ ccmake ..


Scheff'sCat@debian:/mnt/hgfs/hostD/Entwicklung/tests/C++/dll/build-Linux$ cmake --build .
Scanning dependencies of target BigClass
[ 16%] Building CXX object CMakeFiles/BigClass.dir/BigClass.cc.o
[ 33%] Linking CXX shared library libBigClass.so
[ 33%] Built target BigClass
Scanning dependencies of target Test2
[ 50%] Building CXX object CMakeFiles/Test2.dir/Test2.cc.o
[ 66%] Linking CXX executable Test2
[ 66%] Built target Test2
Scanning dependencies of target Test1
[ 83%] Building CXX object CMakeFiles/Test1.dir/Test1.cc.o
[100%] Linking CXX executable Test1
[100%] Built target Test1
Scheff'sCat@debian:/mnt/hgfs/hostD/Entwicklung/tests/C++/dll/build-Linux$ ls
CMakeCache.txt  CMakeFiles  cmake_install.cmake  libBigClass.so  Makefile  Test1  Test2
Scheff'sCat@debian:/mnt/hgfs/hostD/Entwicklung/tests/C++/dll/build-Linux$ ./Test1
Test1:
======
BigClass big;
big.print();
BigClass::print(): *this: BigClass { id: 0 }
big.printLn();
BigClass::print(): *this: BigClass { id: 0 }
BigClass big1(1);
big1.print();
BigClass::print(): *this: BigClass { id: 1 }
big1.printLn();
BigClass::print(): *this: BigClass { id: 1 }
Done.
Scheff'sCat@debian:/mnt/hgfs/hostD/Entwicklung/tests/C++/dll/build-Linux$ ./Test2
Test2:
======
BigClass big;
print(big);
print(): BigClass::print(): *this: BigClass { id: 0 }
printLn(big);
BigClass::print(): *this: BigClass { id: 0 }
BigClass big2(2);
print(big2);
print(): BigClass::print(): *this: BigClass { id: 2 }
printLn(big2);
BigClass::print(): *this: BigClass { id: 2 }
Done.
Scheff'sCat@debian:/mnt/hgfs/hostD/Entwicklung/tests/C++/dll/build-Linux$