【问题标题】:Prohibit inclusion of incompatible headers across compilation units禁止跨编译单元包含不兼容的标头
【发布时间】:2016-01-21 13:11:26
【问题描述】:

这是一个理论问题:假设您有一个库并且它有两个标题。是否有可能使用 C++ 或预处理器宏或两者的组合来实现以下行为:

  1. 依赖项目可以在任意数量的编译单元中包含 Header 1 Header 2 而不会出错。
  2. 从属项目不能包含这两个标头,即使它跨越不同的编译单元。

对于第二种情况,我想要一些会导致某种错误(例如链接器错误)的构造。我不需要给出一个很好的错误消息,我只想禁止在同一个依赖项目中包含两个不兼容的标头。这可能吗?

示例:

Header1.h

// type definitions for foo, Version 1
...

Header2.h

// type definitions for foo, Version 2
...

场景 1:

// (linker) error, versions do not match
CompilationUnit1.cpp <-- Header1.h
CompilationUnit2.cpp <-- Header2.h

场景 2:

// ok, versions match
CompilationUnit1.cpp <-- Header2.h
CompilationUnit2.cpp <-- Header2.h

【问题讨论】:

  • 我认为这不可能!
  • @Eva 你对编译器有什么限制吗?有一种方法,但它不便携。
  • 我倾向于同意。预处理器宏基于每个 TU 应用。唯一可靠生成的链接器错误是“未定义符号”,这不是由包含 more 标头引起的。 ODR 违规是下一个最明显的候选者,即使不能正式诊断。这些失败是因为两个 TU 可能在这里包含相同的标头。
  • @AdrianoRepetti:我们主要使用 Visual Studio 2013,但也有一些(极少数)项目需要使用 gcc 构建。
  • 如果你也写了lib,那么可能有办法。在 lib 中创建一个全局 int(或更高级的 sigleton)。在两个标题中为其提供 extern decls。让每个标头都有一个静态对象,该对象使用一个检查全局值然后设置它的函数进行初始化。每个标题对正确的标题都有自己的想法。如果错了,宾果游戏。

标签: c++


【解决方案1】:

您正在使用 Visual C++,然后可以完成(但请注意,它不可移植,除非您使用 /WX,否则您将收到警告而不是错误)。使用 GCC(也许,我没有尝试)你可以以某种方式使用 #pragma weak

因为每个编译单元都是单独编译的,所以您必须在链接器级别找到 something(导出函数、更改设置)。我发现最简单的方法是声明一个部分并在其上分配一个虚拟变量。如果这个部分在你的头文件中声明了不同的属性,那么链接器会报错。

您必须将其添加到 Header1.h

#pragma section("mylib_priv_impl_section",read)
__declspec(allocate("mylib_priv_impl_section")) static int mylib_priv_impl_var = 0;

然后在Header2.h中:

#pragma section("mylib_priv_impl_section",read,write)
__declspec(allocate("mylib_priv_impl_section")) static int mylib_priv_impl_var = 0;

现在,当您编译和链接时,您将收到以下警告:

发现具有不同属性的多个“mylib_priv_impl_section”部分 (C0300040)

因为我们声明了具有不同属性的相同部分(Header1 中的read 和Header2 中的read+write)。不幸的是,这只是一个警告(您可以使用节名来提供一些有用的诊断信息)并且要停止编译,您必须指定 /WX(很遗憾被 #pragma comment(linker, "/WX") 忽略)。

请注意,我们需要那个 dummy 变量,否则我们声明的部分(如果未使用)将被简单地忽略。除了我们的虚拟变量之外,没有其他东西会放在该部分中(另请参阅Scope of __declspec allocations)。


请注意,如果预处理器宏可行,那么类似于Marco Giordano 建议的解决方案也可以顺利运行。如果您包含 Header1 但您设置要使用 Version2(反之亦然)而不是包含宏,我只会更改它的工作方式以引发错误。像这样的东西(在 Header1 和 Header2 中的镜面反射):

#if MyLibVersion != 1
#error You are including version 1 headers, please set MyLibVersion accordingly.
#endif

【讨论】:

  • 现在我最喜欢你的解决方案,但我对部分不太熟悉。据我了解,以上几行将更改整个翻译单元的部分属性 - 不会弄乱这会让我面临分段错误或其他奇怪/未定义的运行时行为的危险吗?此外,我可能需要两个以上的标题,所以我可能还会使用其他属性(执行、共享、nopage 等)。
  • @eva 不,它只会更改该变量的部分(这就是它存在的原因:如果部分未使用,那么编译器将忽略我们的指令)
  • 您可以使用相同的属性(以免使维护成为噩梦),但可以使用不同的部分。只需在部分名称中包含标题名称(即使仅使用 FILE 宏)
  • @AdrianoRepetti:你确定,它只针对一个变量吗?在页面msdn.microsoft.com/en-us/library/50bewfwa.aspx 上它说:“一旦定义了一个部分,它在编译的其余部分仍然有效。”
  • @AlexanderTobiasHeinrich 是的,它是定义的。来自同一段:“但是,您必须使用 __declspec(allocate) 否则不会在该部分中放置任何内容。”.
【解决方案2】:

不确定您是否在谈论库的通用头文件,例如 vec.h 和 mat.h,并且出于某种原因您想表达规则以避免:

#include <vec.h>
#include <mat.h>

如果是这种情况,我认为这是不可能的,另一方面,如果您正在谈论来自不同库版本的相同标头,例如 vec_2_0.h , vec_2_1.h 我会以不同的方式谈论问题(首先最重要的是,如果我可以命名标题,我不会在标题本身上写版本)。

我要解决的方法是在文件夹结构示例中分离包含:

mathlib:

-----> mathlib_2_0
---------> includes
----> mathlib_2_1
-------->  includes

然后,您可以在项目设置中强制执行以避免选择其中一个,而不是尝试在文件本身中修复它。

也许您可以做的(但我认为这很难看)是将所有可能的标头包含在一个宏中,并使用定义版本让处理器只留下正确的标头。 示例:

Compiler flag -DMathVersion 2_0

宏:

INCLUDE_VECTOR_HEADER()
{
#if MathVersion == 2_0
#include <vec_2_0.h>
#elif MathVersion == 2_1
#include <vec_2_1.h>
#endif
}

但我认为这有点矫枉过正而且有点难看,也许可以概括为将版本作为宏的参数。我个人不会走这条路。 PS:全部是伪代码,仅供参考

【讨论】:

  • 我喜欢在构建过程中阻止它的一般想法,而不是使用语言机制。这可能会以他(和我)没有想到的方式解决 OP 的问题。
  • 我明白了,如果您想要一种“与语言无关”的方式,那么也许可以使用预构建脚本?现在我不知道是否可能,这主要是一个疯狂的猜测,你可以做的是有一个python(或其他语言)脚本,解析文件并检查你想要的规则,创建一个不难匹配代码的通用规则系统。这种方法的缺点是在编译时花费了额外的时间,如果可能触发这样的脚本,触发器可能因编译器而异。再说一次,只是猜测不是我的专业领域。
【解决方案3】:

运行时检查仅适用于语言。 (在另一篇文章中,我会建议一些构建时检查。)

这并不优雅。我有一种挥之不去的感觉,有人会为此想出一个 2-liner。但是为了它的价值。

每个标题定义一个类,当程序启动时,该类将为每个翻译单元实例化一次。 (我们需要一个类,因为我们需要运行 ctor 代码来检查一个值。)每个类的 ctor 读取一个全局 sentinel 并检查它是否具有错误的魔法值(它最初是 0 作为全局) .如果没有,它会分配自己的。它不依赖于静态对象的初始化顺序。我不确定我们是否需要在这里保护哨兵免受并发访问;我希望不会。

lib.cpp(你的库):

int sentinel;
// other lib stuff
// ...

f1.h(两个头文件之一):

#include<iostream>
#include<cstdlib>
using namespace std;

extern int sentinel;

struct f1duplGuard
{
    enum { f1=0xf1, f2= 0xf2 };

    f1duplGuard() 
    { 
        cout << "f1 duplGuard() " << endl;
        if( ::sentinel == f2 ) 
        {
            cerr << "f1: include violation -- must be a f2 somewhere" << endl;
            exit(1); 
        }
        sentinel = f1;
    }
};

static f1duplGuard dg;

f2.h(另一个头文件——具有不同常量的镜像):

#include<iostream>
#include<cstdlib>
using namespace std;

extern int sentinel;

struct f2duplGuard
{
    enum { f1=0xf1, f2= 0xf2 };

    f2duplGuard() 
    { 
        cout << "f2 duplGuard() " << endl;

        if( ::sentinel == f1 ) 
        {
            cerr << "f2: include violation -- must be a f1 somewhere" << endl;
            exit(1); 
        }
        sentinel = f2;
    }
};

static f2duplGuard dg;

一个使用 lib 的 TU,包括两个 f 头文件之一

#include <iostream>
#include "f2.h"      // changing this to f1.h fails at run time

using namespace std;


void f(void)
{
    cout << "second.cpp, f()" << endl;
}

使用 lib 的第二个 TU,也包括一个 f 标头(使用 main):

#include <iostream>
#include "f2.h"

extern void f();

using namespace std;

int main(void)
{
    f();
    return 0;
}

呸。如果将两个包含中的一个更改为另一个标头,则会触发错误消息并退出。

示例会话(没有错误):

$ g++ -O0 -std=c++14  -o dupl-static -Wall second.cpp dupl-static.cpp lib.cpp && ./dupl-static
f2 duplGuard()
f2 duplGuard()
second.cpp, f()

有错误的示例会话:

 cat dupl-static.cpp && g++ -std=c++14  -o dupl-static -Wall second.cpp dupl-static.cpp lib.cpp && ./dupl-static
#include <iostream>
#include "f1.h"

extern void f();

using namespace std;

int main(void)
{
        f();
}
f1 duplGuard()
f2 duplGuard()
f2: include violation -- must be a f1 somewhere

【讨论】:

  • 我一直在寻找会产生构建时错误的东西,但毕竟,在运行开始时保证运行时失败仍然可以完成这项工作。但是,我有一个问题:在您的 c'tor 中,您在初始化之前访问哨兵变量,不是吗?这不太可能,但正如我所见,::sentinel == f1 可以评估为true,即使::sentinal 尚未初始化......我是在监督什么还是这是未定义的行为?无论如何,为你的努力 +1!
  • 不,全局变量“在任何其他初始化发生之前进行零初始化”(C++11 标准,3.6.3/2),因为它们具有静态存储持续时间。
【解决方案4】:

我在构建时看到了几种方法来做到这一点(但在编译器/链接器之外的机制的帮助下)。

预编译:Use gcc's dependency generator.

gcc 可以选择在命令行上遍历每个文件的包含树并列出其依赖项,其中包括直接或间接包含的头文件。显然这需要gcc,但它实际上不会编译任何东西,所以它可能是VS中带有cygwin gcc的预构建命令。可能只需要最少的配置,因为选项-MG 让 gcc 可以优雅地处理“丢失”的标头。 (为了遵循嵌套包含,有必要使用-I 来定义包含路径。)

例如,假设我们有三个文件。
1.c:

#include "header1.h"

2.c:

#include <stdio.h>
#include "2.h"

和2.h:

#include "header2.h"

示例会话:

$ ls && echo "-------" && gcc -MM -MG *.c
1.c  2.c  2.h
-------
1.o: 1.c header1.h
2.o: 2.c 2.h header2.h

这可以简单地用于标题名称。

编译时间:

在构建期间定义一个 C 预处理器令牌并在标头中检查它

同意两个众所周知的令牌,其中一个将由构建环境为任何构建定义;标头使用#ifdef 对它们进行检查。例如,header1.h 可以包含该行

#ifdef USE_HEADER2
#   error "Using header1.h although USE_HEADER2 is defined"
#endif

在 make 环境中,可以通过传递 -D 选项来定义 USE_HEADER2,例如 make -DUSE_HEADER_1 ...` 等。在 Visual Studio 中,可以在项目的 Build 属性中定义符号。

这不适用于增量构建,因为在构建之间设置可能会发生变化。这肯定是错误的根源。对于大型项目而言,仅将完全重建作为补救措施是禁止的。

Post-Compile:在每个标头中定义一个静态字符串并为其生成二进制文件

这受到@MSalters 在 cmets 中的建议的启发,但可能更简单一些。在每个标头中定义一个不同的静态字符串。对于header1.h,可能是

volatile static char str[] = "HEADER_1_INCLUDED";

(如果没有volatile,编译器会在使用优化构建时忽略从未使用过的字符串。这可能是特定于实现的。我使用了 gcc 4.9.3。)

构建后,您只需检查所有库和目标文件中的两个字符串,如果两者都存在则失败:

 if grep -q  "HEADER_1_INCLUDED" *.o *.a *.lib *.dll &&  grep -q  "HEADER_2_INCLUDED" *.o *.a *.lib *.dll; 
 then handle_error; 
 fi

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-01-17
    • 2015-03-23
    • 2016-11-20
    • 1970-01-01
    • 2022-11-05
    • 2013-09-20
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多