什么是编译时检测库代码和用户代码之间不匹配的预处理器定义的好技术?

What is a good technique for compile-time detection of mismatched preprocessor-definitions between library-code and user-code?

激励背景信息:我维护一个 C++ 库,这个周末我花了太多时间在链接到这个库的应用程序中追踪一个神秘的内存损坏问题。问题最终被证明是由于 C++ 库是使用特定的 -DBLAH_BLAH 编译器标志构建的,而应用程序的代码在没有 -DBLAH_BLAH 标志的情况下编译,这导致了库代码和应用程​​序代码在数据布局方面对库头文件中声明的 类 的解释不同。即:sizeof(ThisOneParticularClass) 在从应用程序中的 .cpp 文件调用时会 return 与从库中的 .cpp 文件调用时不同的值。

到目前为止,很不幸——我已经通过确保库和应用程序都使用相同的预处理器标志构建来解决了眼前的问题,并且我还修改了库,以便存在或不存在-DBLAH_BLAH 标志不会影响 sizeof() 它导出的 类... 但我觉得这还不足以解决使用不同预处理器编译库的更普遍的问题-flags 比使用该库的应用程序。理想情况下,我想找到一种机制,可以在编译时捕获此类问题,而不是让它在运行时静默调用未定义的行为。有没有好的技术可以做到这一点? (我能想到的就是自动生成一个头文件,其中包含 #ifdef/#ifndef 测试应用程序代码到 #include,如果必要 #defines 会故意 #error没有设置,或者可能会自动设置适当的 #defines 就在那里......但这感觉很像重新发明 automake 和类似的东西,这似乎可能会打开一大堆蠕虫)

实现这种检查的一种方法是根据是否定义了特定的 macros/tokens,为发生变化的全局变量提供 definition/declaration 对。如果客户端源包含的 header 中的声明与构建库时使用的声明不匹配,那么这样做将导致链接器错误。

作为简要说明,请考虑以下部分,将其添加到“MyLibrary.h”header 文件(在构建库和使用库时都包含):

#ifdef FOOFLAG
extern int fooflag;
static inline int foocheck = fooflag;    // Forces a reference to the above external
#else
extern int nofooflag;
static inline int foocheck = nofooflag;  //                <ditto>
#endif

然后,在您的库中,在单独的“.cpp”模块或现有模块中添加以下代码:

#include "MyLibrary.h"

#ifdef FOOFLAG
int fooflag = 42;
#else
int nofooflag = 42;
#endif

这将(或应该)确保可执行文件的所有组件源文件都使用与FOOFLAG 标记相同的“状态”进行编译。在链接到 object 库时我还没有实际测试过它,但它在从两个不同的源构建 EXE 文件时有效:它只会在 both都没有-DFOOFLAG选项;如果一个有而另一个没有,则链接器失败并显示(在 Visual Studio/MSVC 中):

error LNK2001: unresolved external symbol "int fooflag" (?fooflag@@3HA)

主要问题是错误消息不是特别有用(对您图书馆的 third-party 用户);可以通过适当使用这些检查变量的名称来改善(也许)。1

一个优点是系统易于扩展:可以添加所需数量的检查变量(每个关键宏标记一个),同样的想法也可以用于检查实际的 values 的宏,代码如下:

#if FOOFLAG == 1
int fooflag1 = 42;
#elif FOOFLAG == 2
int fooflag2 = 42;
#elif FOOFLAG == 3
int fooflag3 = 42;
#else
int fooflagX = 42;
#endif

1 例如,沿着这些线的东西(在 header 文件中进行适当的修改):

#ifdef FOOFLAG
int CANT_DEFINE_FOOFLAG = 42;
#else
int MUST_DEFINE_FOOFLAG = 42;
#endif

重要说明: 我刚刚使用 clang-cl 编译器(在 Visual Studio 2019 年)尝试了这种技术,但链接器未能捕捉到不匹配,因为它完全优化了对 foocheck 变量的所有引用(因此,对从属 fooflag 的引用)。但是,有一个相当简单的解决方法,使用 clang 的 __attribute__((used)) 指令(它也适用于 GCC C++ 编译器)。这是显示的最后一个代码片段的 header 部分,添加了解决方法:

#if defined(__clang__) || defined(__GNUC__)
#define KEEPIT __attribute__((used))
// Equivalent directives may be available for other compilers ...
#else
#define KEEPIT
#endif

#ifdef FOOFLAG
extern int CANT_DEFINE_FOOFLAG;
KEEPIT static inline int foocheck = CANT_DEFINE_FOOFLAG; // Forces reference to above
#else
extern int MUST_DEFINE_FOOFLAG;
KEEPIT static inline int foocheck = MUST_DEFINE_FOOFLAG; //         <ditto>
#endif

作为@adrian(优秀)答案的替代方案,这里有一个 运行时 检查的建议,可能会引起人们的兴趣。

为了举例,我们假设有两个标志,FOO1 和 FOO2。首先,为了让我的方案起作用,并且由于 OP 似乎使用 #ifdef 而不是 #if,库需要提供一个如下所示的头文件(为清楚起见省略了头文件) :

// MyLibrary_config_check.h

#ifdef FOO1
    #define FOO1_VAL 1
#else
    #define FOO1_VAL 0
#endif

#ifdef FOO2
    #define FOO2_VAL 1
#else
    #define FOO2_VAL 0
#endif

... etc ...

然后,同一个头文件声明了以下函数:

bool CheckMyLibraryConfig (int expected_flag1, int expected_flag2 /* , ... */);

库然后像这样实现它:

bool CheckMyLibraryConfig (int expected_flag1, int expected_flag2 /* , ... */)
{
    static const int configured_flag1 = FOO1_VAL;
    static const int configured_flag2 = FOO2_VAL;
    // ...

    if (expected_flag1 != configured_flag1)
        return false;
    if (expected_flag2 != configured_flag2)
        return false;
    // ...
    return true;
}

然后图书馆的消费者可以这样做:

    if (!CheckMyLibraryConfig (FOO1_VAL, FOO2_VAL /* , ... */))
        halt_and_catch_fire ();

不利的一面是,它是运行时检查,而这不是所要求的。从好的方面来说,CheckMyLibraryConfig 可以这样实现:

std::string CheckMyLibraryConfig (int expected_flag1, int expected_flag2 /* , ... */)
{
    if (expected_flag1 != configured_flag1)
        return std::string ("Expected value of FOO1 does not match configured value, expected: ") + std::to_string (expected_flag1) + ", configured: " + std::to_string (expected_flag2);

    ...

    return "";
}

然后消费者可以检查并显示返回的任何 non-empty 字符串。随心所欲(该代码当然可以分解得更好)并在返回报告所有 mis-matches 的字符串之前检查所有标志,发疯吧。

在 Microsoft C++ 前端和 linker,#pragma detect_mismatch directive can be used in a very similar spirit as the solution presented in 。就像那个答案一样,不匹配是在 link 时检测到的,而不是在编译时。它“在对象中放置一条记录。linker 检查这些记录是否存在潜在的不匹配。”

比如在不同编译单元中包含的头文件中:

#ifdef BLAH_BLAH
#pragma detect_mismatch("blah_blah_enabled", "true")
#else
#pragma detect_mismatch("blah_blah_enabled", "false")
#endif

尝试 link 具有不同“blah_blah_enabled”值的目标文件将失败并返回 LNK2038:

mismatch detected for 'name': value 'value_1' doesn't match value 'value_2' in filename.obj

根据问题中提到的 automake,我假设提问者没有使用 Microsoft C++ 工具链。我将其张贴在这里,以防它能帮助处于类似情况的正在使用该工具链的人。

我相信 Adrian Mole 的回答中最接近 __attribute__((used)) 的 MSVC 类似物是 /INCLUDE:<i>symbol-name</i> linker option, which can be injected from a compilation unit via #pragma comment(linker, "/include:<i>symbol-name</i>")