Jer*_*ner 12 c++ preprocessor static-libraries
激励背景信息:我维护一个 C++ 库,这个周末我花了太多时间来追踪链接到该库的应用程序中的神秘内存损坏问题。该问题最终被证明是由于 C++ 库是使用特定的-DBLAH_BLAH编译器标志构建的,而应用程序的代码是在没有该-DBLAH_BLAH标志的情况下编译的,这导致库代码和应用程序代码解释库的头文件中声明的类在数据布局方面有所不同。也就是说:sizeof(ThisOneParticularClass)从应用程序中的 .cpp 文件调用时返回的值与从库中的 .cpp 文件调用时返回的值不同。
到目前为止,非常不幸 - 我已经通过确保库和应用程序都使用相同的预处理器标志构建来解决了眼前的问题,并且我还修改了库,以便标志的存在或不存在-DBLAH_BLAH不会影响它sizeof()导出的类...但我觉得这并不足以解决使用与使用该库的应用程序不同的预处理器标志编译的库的更普遍的问题。理想情况下,我希望找到一种机制,可以在编译时捕获此类问题,而不是允许它在运行时默默地调用未定义的行为。有没有好的技术可以做到这一点?(我能想到的就是自动生成一个头文件,其中包含#ifdef/#ifndef对应用程序代码的测试#include,如果未设置#error必要的 s,则该头文件会故意排除,或者可能会自动在那里设置适当的 s...但是这感觉很像重新发明和类似,这似乎有可能打开一大堆蠕虫)#define#defineautomake
Adr*_*ica 10
实现这种检查的一种方法是根据是否定义了特定的宏/标记来为发生变化的全局变量提供定义/声明对。如果客户端源包含的标头中的声明与构建库时使用的声明不匹配,则这样做将导致链接器错误。
作为简要说明,请考虑以下部分,将其添加到“MyLibrary.h”头文件中(在构建库和使用库时都包含):
#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
Run Code Online (Sandbox Code Playgroud)
然后,在您的库中,在单独的“.cpp”模块或现有模块中添加以下代码:
#include "MyLibrary.h"
#ifdef FOOFLAG
int fooflag = 42;
#else
int nofooflag = 42;
#endif
Run Code Online (Sandbox Code Playgroud)
这将(或应该)确保可执行文件的所有组件源文件都使用令牌的相同“状态”进行编译FOOFLAG。在链接到对象库时,我实际上没有测试过这一点,但它在从两个单独的源构建 EXE 文件时起作用:只有当两者都没有该-DFOOFLAG选项时它才会构建;如果一个有但另一个没有,则链接器失败并显示(在 Visual Studio/MSVC 中):
错误 LNK2001:无法解析的外部符号“int fooflag”(?fooflag@@3HA)
这样做的主要问题是错误消息并不是特别有帮助(对于您的库的第三方用户);(或许)可以通过适当使用这些检查变量的名称来改善这一问题。1
一个优点是系统易于扩展:可以根据需要添加尽可能多的此类检查变量(每个关键宏标记一个),并且相同的想法也可以用于检查所述宏的实际值,代码如下下列的:
#if FOOFLAG == 1
int fooflag1 = 42;
#elif FOOFLAG == 2
int fooflag2 = 42;
#elif FOOFLAG == 3
int fooflag3 = 42;
#else
int fooflagX = 42;
#endif
Run Code Online (Sandbox Code Playgroud)
1例如,类似以下内容(在头文件中进行适当修改):
#ifdef FOOFLAG
int CANT_DEFINE_FOOFLAG = 42;
#else
int MUST_DEFINE_FOOFLAG = 42;
#endif
Run Code Online (Sandbox Code Playgroud)
重要提示:我刚刚使用 clang-cl 编译器(在 Visual Studio 2019 中)尝试了这种技术,链接器未能捕获不匹配,因为它完全优化了对变量的所有引用foocheck(以及对依赖项的引用fooflag) 。然而,有一个相当简单的解决方法,使用 clang 的__attribute__((used))指令(也适用于 GCC C++ 编译器)。以下是显示的最后一个代码片段的标题部分,并添加了该解决方法:
#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
Run Code Online (Sandbox Code Playgroud)
在 Microsoft C++ 前端和链接器中,该#pragma detect_mismatch指令的使用方式与Adrian Mole 的回答中提出的解决方案非常相似。就像那个答案一样,不匹配是在链接时检测到的,而不是在编译时检测到的。它“将记录放置在对象中。链接器检查这些记录是否存在潜在的不匹配。”
假设不同编译单元中包含的头文件中包含这样的内容:
#ifdef BLAH_BLAH
#pragma detect_mismatch("blah_blah_enabled", "true")
#else
#pragma detect_mismatch("blah_blah_enabled", "false")
#endif
Run Code Online (Sandbox Code Playgroud)
尝试链接具有不同“blah_blah_enabled”值的目标文件将失败并显示LNK2038:
检测到“ name ”不匹配:值“ value_1 ”与filename.obj中的值“ value_2 ”不匹配
根据automake问题中提到的,我假设提问者没有使用 Microsoft C++ 工具链。我将其发布在这里,以防它对使用该工具链的类似情况的人有所帮助。
__attribute__((used))我相信与Adrian Mole 的答案最接近的 MSVC 类似物是链接器选项,它可以通过从编译单元注入。/INCLUDE:symbol-name#pragma comment(linker, "/include:symbol-name")
作为@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 ...
Run Code Online (Sandbox Code Playgroud)
然后,同一个头文件声明以下函数:
bool CheckMyLibraryConfig (int expected_flag1, int expected_flag2 /* , ... */);
Run Code Online (Sandbox Code Playgroud)
然后库会像这样实现:
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;
}
Run Code Online (Sandbox Code Playgroud)
然后图书馆的使用者可以执行以下操作:
if (!CheckMyLibraryConfig (FOO1_VAL, FOO2_VAL /* , ... */))
halt_and_catch_fire ();
Run Code Online (Sandbox Code Playgroud)
不利的一面是,这是一个运行时检查,而这并不是我们所要求的。从好的方面来说,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 "";
}
Run Code Online (Sandbox Code Playgroud)
然后,消费者可以检查并显示返回的任何非空字符串。随心所欲(该代码当然可以更好地分解)并在返回报告所有不匹配的字符串之前检查所有标志,疯狂吧。
| 归档时间: |
|
| 查看次数: |
1031 次 |
| 最近记录: |