简体   繁体   中英

Prohibit inclusion of incompatible headers across compilation units

This is a theoretical question: Assume you have a library and it has two headers. Is there any possibility with C++ or with preprocessor macros or a combination of both to achieve the following behaviour:

  1. Dependent projects can include Header 1 or Header 2 in any number of compilation units without error.
  2. Dependent Projects cannot include both Headers even if its accross different compilation units.

I'd like to have some construct that would result in some kind of error (eg a linker error) for the 2nd case. I does not need to give a nice error message, I just want to prohibt inclusion of two incompatible headers in the same dependent project. Is that possible?

EXAMPLE:

Header1.h

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

Header2.h

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

Scenario 1:

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

Scenario 2:

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

not sure if you are talking about generic headers of a library, for example vec.h and mat.h, and for some reason you want to express rules to avoid to have:

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

If that is the case I don't think that is possible, on the other hand if you are talking about same header from different library version, like vec_2_0.h , vec_2_1.h I would talke the problem differently (well first of all if I could name the headers I would not write the version on the header itself).

The way I would tackle is by separating the includes in the folder structure example:

mathlib:

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

Then you can enforce at project settings to avoid picking one over the other, rather then trying to fix that in the file itself.

Maybe what you can do (but I think is ugly) is to wrap all the possible header include in a macro and use a define version to have the processor leaving only the correct one. Example:

Compiler flag -DMathVersion 2_0

The macro:

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

But I think is way overkill and a bit ugly, maybe can be generalized in having the version as parameter of the macro. I would not go down that road personally. PS: All is pseudo code just to give an idea

A run time check is doable language-only. (In another post I'll suggest a number of build-time checks.)

This is not elegant. I have a nagging feeling that somebody will come up with a 2-liner for this. But for what it's worth.

Each header defines a class which will be instantiated once for each translation unit when the program starts. (We need a class because we need to run ctor code in order to check a value.) The ctor of each class reads a global sentinel and checks whether it has the wrong magic value (it will be 0 initially as a global). If not, it assigns its own. It does not rely on the initialization order of the static objects. I am not sure whether we need to protect sentinel from concurrent access here; I hope not.

lib.cpp (your library):

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

f1.h (one of the two headers):

#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 (the other header -- the moirror image with a different constant):

#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;

One TU using the lib, including one of the two f headers

#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;
}

The second TU using the lib, including an f header, too (with main):

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

extern void f();

using namespace std;

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

Phew. If you change one of the two includes to the other header, you trigger an error message and exit.

Sample session (without the error):

$ 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()

Sample session with error:

 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

You're working with Visual C++ then it can be done (but be warned that it's not portable and you will get warnings instead of errors unless you use /WX ). With GCC (maybe, I didn't try) you may use somehow #pragma weak .

Because each compilation unit is compiled separately then you have to find something at linker level (exporting a function, changing a setting). Easiest way I found is to declare a section and allocate a dummy variable on it. If this section is declared with different attributes on your headers then linker will complain.

You have to add this to Header1.h :

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

And then in Header2.h :

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

Now when you compile and link you will get this warning:

multiple 'mylib_priv_impl_section' sections found with different attributes (C0300040)

Because we declared same section with different attributes ( read in Header1 and read+write in Header2). Unfortunately it's just a warning (you may use section name to give some useful diagnostic message) and to stop compilation you must specify /WX (unfortunately ignored by #pragma comment(linker, "/WX") ).

Note that we need that dummy variable otherwise our declared section (if unused) will be simply ignored. Nothing else but our dummy variable will be placed in that section (see also Scope of __declspec allocations ).


Note that if a preprocessor macro is viable then also a solution similar to what Marco Giordano suggested will work smoothly. I'd just change the way it work to throw an error if you're including Header1 but you set you want to use Version2 (and vice-versa) instead of having a macro for inclusion. Something like this (in Header1 and specular in Header2):

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

I see several ways to do that at build time (but with the help of mechanisms beyond just the compiler/linker).

Pre-compile: Use gcc's dependency generator.

gcc has an option to walk the include tree for each file on the command line and list its dependencies, among them the directly or indirectly included header files. Obviously this needs gcc, but it won't actually compile anything, so it could be a pre-build command in VS with a cygwin gcc. Probably only minimal configuration is necessary because the option -MG lets gcc handle "missing" headers gracefully. (In order to follow nested includes it will be necessary to use -I in order to define include paths.)

As an example let's assume we have three files.
1.c:

#include "header1.h"

2.c :

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

and 2.h:

#include "header2.h"

Sample session:

$ 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

This can be simply grepped for the header names.

Compile time:

Define a C preprocessor token during the build and check it in the headers

Agree on two well-known tokens, of which exactly one will be defined by the build environment for any build; the headers check against them with an #ifdef . For example, header1.h could contain the line

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

In a make environment one could define USE_HEADER2 by passing a -D option, like make -DUSE_HEADER_1 ...` or the like. In Visual Studio one would define the symbol in the Build properties of the project.

This will not work for incremental builds, because between builds the setting might change. That is surely a source of errors; only doing complete rebuilds as a remedy is prohibitive for large projects.

Post-Compile: Define a static string in each header and grep the binaries for it

This is inspired by @MSalters' suggestion in the comments, but possibly a bit simpler. Define a distinct static string in each header. For header1.h , that could be

volatile static char str[] = "HEADER_1_INCLUDED";

(Without the volatile the compiler would dismiss the never-used string when built with optimization. This could be implementation specific. I used gcc 4.9.3.)

After a build you just check all library and object files for both strings and fail if both are there:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM