禁止在编译单元中包含不兼容的标头

时间:2016-01-21 13:11:27

标签: c++

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

  1. 依赖项目可以在任意数量的编译单元中包含标题1 标题2而不会出错。
  2. 依赖项目不能包含两个标题,即使它不同的编译单元。
  3. 我希望有一些构造可能会导致第二种情况出现某种错误(例如链接器错误)。我不需要提供一个很好的错误消息,我只想禁止在同一依赖项目中包含两个不兼容的标头。这可能吗?

    示例:

    那么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
    

4 个答案:

答案 0 :(得分: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:所有都是伪代码只是为了给出一个想法

答案 1 :(得分:2)

运行时检查只能使用语言。 (在另一篇文章中,我将建议一些构建时间检查。)

这不优雅。我有一种唠叨的感觉,有人会为此想出一个2线。但是它的价值在哪里。

每个标题定义一个类,当程序启动时,该类将为每个翻译单元实例化一次。 (我们需要一个类,因为我们需要运行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(另一个标题 - 具有不同常量的moirror图像):

#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

答案 2 :(得分:2)

您正在使用Visual C ++,然后可以完成(但要注意它不可移植,除非您使用{{1},否则您将收到警告而不是错误}})。有了海湾合作委员会(也许,我没有尝试过)你可能会以某种方式使用/WX

因为每个编译单元是单独编译的,所以你必须在链接器级别找到某些东西(导出一个函数,更改一个设置)。我发现最简单的方法是声明一个部分并在其上分配一个虚拟变量。如果在标题上声明此部分具有不同的属性,那么链接器将会抱怨。

您必须将此添加到 Header1.h

#pragma weak

然后在 Header2.h

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

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

  

multiple&#39; mylib_priv_impl_section&#39;找到具有不同属性的部分(C0300040)

因为我们使用不同的属性声明了相同的部分(Header1中为#pragma section("mylib_priv_impl_section",read,write) __declspec(allocate("mylib_priv_impl_section")) static int mylib_priv_impl_var = 0; ,Header2中为read)。不幸的是,它只是一个警告(您可以使用部分名称来提供一些有用的诊断消息)并且要停止编译,您必须指定read+write(遗憾的是/WX忽略了它)。

请注意,我们需要 dummy 变量,否则我们声明的部分(如果未使用)将被忽略。除了我们的虚拟变量之外别无其他任何东西(参见Scope of __declspec allocations)。

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

#pragma comment(linker, "/WX")

答案 3 :(得分:0)

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

预编译: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环境中,可以通过传递USE_HEADER2选项来定义-D,例如make -DUSE_HEADER_1 ......等。在Visual Studio中,可以在项目的Build属性中定义符号。

这对增量构建不起作用,因为在构建之间,设置可能会更改。这肯定是错误的根源;只做完整的重建作为补救措施对大型项目来说是禁止的。

编译后:在每个标题中定义一个静态字符串并为其创建二进制文件

这是受到@MSalters在评论中的建议的启发,但可能有点简单。在每个标头中定义不同的静态字符串。对于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