1

I am working on a large project and there is a BigHeader.h which is very costly to include compile time wise, and a BigHeaderFwd.h. I want to enforce that users don't do this:

// SomeRandomFile.h
#include "BigHeader.h"

void useFunc(const BigHeader& bh);

How can I write a compile time / preprocessor time logic inside BigHeader.h to the effect of

#if included in .h file
# error, with message "please include BigHeaderFwd instead"
# else
 // no errror

I want to avoid external scripts if possible

Thanks

Michael
  • 172
  • 12
  • So including the BigHeaderFwd is the right way and would result in whatever people want to achieve by including BigHeader.h (plus something to avoid the costs of including multiple times but ultimatly in including once)? Then I think I have an idea. – Yunnosch Jul 04 '23 at 08:46
  • Would you be interested in a concept on top of what you seem to plan, which involves only including parts of the BigHeader, which users have to state beforehand? That was a surprisingly successful concept where I worked, when we had a similar problem. – Yunnosch Jul 04 '23 at 08:50
  • Thanks for the smile, accompanied by some respect for your way of doing things. @463035818_is_not_an_ai – Yunnosch Jul 04 '23 at 08:51
  • Obviously they might need the definition in the .cpp file but I'm trying to avoid including inside .h files for starters – Michael Jul 04 '23 at 08:53
  • 1
    This is probably hard to achieve without any additional tool. At best, you can define a macro in `BigHeaderFwd.h` and check in `BigHeader.h` whether `BigHeaderFwd.h` has been used as well. This still would require that the translation unit (the `.cpp` file) includes `BigHeader.h` last or at least after the header where `BigHeaderFwd.h` has been included, and it doesn't prohibit the usage of `BigHeader.h` in headers in general. – Scheff's Cat Jul 04 '23 at 08:53
  • 3
    This looks like an XY problem. You ask about some preprocessor trickery while it seems that you are actually interested in reducing compilation times. SA https://stackoverflow.com/questions/46319579/what-are-the-drawbacks-of-single-source-project-structures/46321758#46321758 – user7860670 Jul 04 '23 at 09:33

3 Answers3

1

How can I write a compile time / preprocessor time logic inside BigHeader.h to the effect of

Create a macro that will have to be added every time big header is included.

// BigHeader.h
#ifndef I_KNOW_WHAT_I_AM_DOING_AND_WANT_TO_INCLUDE_BIG_HEADER_H
#error "please include BigHeaderFwd instead"
#endif

// SomeRandomFile.h
#include "BigHeader.h" //error

// SomeRandomApprovedFile.h
#define I_KNOW_WHAT_I_AM_DOING_AND_WANT_TO_INCLUDE_BIG_HEADER_H
#include "BigHeader.h"

You may be interested in feature_test_macros . Similar method is used in glibc bits headers directory.

KamilCuk
  • 120,984
  • 8
  • 59
  • 111
  • That would still allow including BigHead multiple times other than using Fwd, after doing it once as intended. Wouldn't it? – Yunnosch Jul 04 '23 at 08:56
  • `Wouldn't it?` Yes. Code review. – KamilCuk Jul 04 '23 at 08:57
  • I appreciate the answer but if code review was the solution to my problem I would not have bothered asking in the first place – Michael Jul 04 '23 at 09:43
  • It does not solve my problem because the user has to be aware and leaky macros in headers can ruin this effort – Michael Jul 04 '23 at 09:43
  • 2
    @Michael a malicious developer can bypass any measure you take. This answer is the compiler error you ask for. Compare to what `private` is: Is it possible to bypass it and access a private member if you really want it? Yes. It is sane to do so? No, and if someone does it, you should have a talk with them. – 463035818_is_not_an_ai Jul 04 '23 at 09:46
  • @463035818_is_not_an_ai I understand your reply, it is irrelevant for my use case – Michael Jul 04 '23 at 09:54
  • This solution leads to recursive problem. Including `SomeRandomApprovedFile.h` now leads to the same issues as including `BigHeader.h` directly. – user7860670 Jul 04 '23 at 09:54
  • @Micheal if this answer is not relevant for your use case then it is unclear what would be relevant. – 463035818_is_not_an_ai Jul 04 '23 at 09:55
  • @user7860670 the symbol should be `undef`fed after including the BigHeader – 463035818_is_not_an_ai Jul 04 '23 at 09:56
  • 1
    ¿But how does this `undef` help preventing accidental includes of `SomeRandomApprovedFile.h`? Everyone can just do `// SomeRandomFile.h #include "SomeRandomApprovedFile.h"` – user7860670 Jul 04 '23 at 09:58
  • @yunnosch Not if you put #undef I_KNOW_WHAT_I_AM_DOING_AND_WANT_TO_INCLUDE_BIG_HEADER_H after the #include "BigHeader.h". – stackoverblown Jul 04 '23 at 11:40
1

1. Allow including only with a special definition

This solution has already been proposed by @KamilCuk. In essence, error if the header was included without some I_KNOW_WHAT_I_AM_DOING_AND_WANT_TO_INCLUDE_BIG_HEADER_H definition prior to the #include.

Of course, an incredibly stupid, or malicious developer can bypass this restriction and still include the header.

2. Hide the header as well as possible

You can locate the header inside a secret folder in your include/ directory, or just outside of it. If the headers of your library are located in mylib/include, you can put the header in:

  • mylib/include/mylib/implementation_secrets/BigHeader.h, which makes it possible for your headers to include it, but would make it less convenient for other developers to do it
  • mylib/secret_include/BigHeader.h, in which case you can include it via relative paths from other headers, e.g. #include "../../secret_include/BigHeader.h"

Of course, a malicious developer can still include it, even if it's in a weird place.

3. Force linker errors

You can exploit the fact that external symbols can only be defined in one translation unit, unless they are inline:

// BigHeader.h
int YOU_MUST_NOT_INCLUDE_ME_YOU_FOOL = 0;

The result is that if BigHeader.h is included in more than one place, you will get a linker error. Obviously, this is only somewhat useful, because it also breaks you own code if you include it more than once.

4. Whitelist source files

You can use GCC's __BASE_FILE__ macro which expands to the name of the cpp file which is currently compiled. Force a compiler error if this is not what you expect.

// BigHeader.h

constexpr std::string_view includer = __BASE_FILE__ ;
static_assert(includer == "blessed_file.cpp",
              "Cannot include BigHeader.h from " __BASE_FILE__);

However, even with this method, a malicious or stupid developer can rename their source file to blessed_file.cpp and bypass this restriction.

5. Count includes

Perhaps the most over-engineered solution yet:

// BigHeader.h

inline int count_include() {
    constexpr int max_includes = 3;
    static int counter = 0;

   if (++counter > max_includes) {
       throw ":("; // TODO: throw an exception, call std::terminate, etc.
   }
   return counter;
}

// dynamic initialization, one function call per including TU
static const int call_count_include = count_include();

In every cpp file which includes BigHeader.h, the definition of count_include() is always the same, and the same counter is incremented. However, the definition of call_count_include is unique for each cpp file, and count_include() will be called once for each.

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
  • I really like 4 and 5 ideas. In 5 it is a bit hard to make a useful error, although it is very good at enforcing. 4 is almost what I need, but I would have loved to have this same check for specific `.h` files that include this file. I know this is difficult to achieve because C++ works with translation units, but are you familiar with a way to do this? – Michael Jul 04 '23 at 11:28
  • @Michael no, the problem is that header files "disappear" when the get included by the pre-processor. The PP only keeps track of the current header file, and the including source file, not the full stack. For a full list of available macros, check: https://gcc.gnu.org/onlinedocs/cpp/Common-Predefined-Macros.html – Jan Schultke Jul 04 '23 at 12:06
0

If your goal is to avoid long compilation times, then simply don't merge commits that increase compilation time over a certain threshold. Otherwise read on.

You can create a C++ header file that is difficult to include in an .h file by mistake. One major downside is that you need additional syntax. There are other downsides as well.

#ifndef BigHeader_H
#define BigHeader_H

#include <source_location>

// some declarations

// a constexpr version of strlen
constexpr size_t ce_strlen(const char* s)
{
  std::size_t l = 0;
  while(*s++) l++;
  return l;
}

#define CONCAT(A,B) CONCAT2(A,B)
#define CONCAT2(A,B) A##B

#define VALIDATE_BIG_HEADER \
  namespace CONCAT(bigheader_detail_, __LINE__) { \
  static constexpr std::source_location loc = std::source_location::current(); \
  static constexpr char const * const f = loc.file_name(); \
  static constexpr std::size_t len = ce_strlen(loc.file_name()); \
  static_assert(len > 2 && !(f[len-1] == 'h' && f[len-2] == '.'), \
                 "BigHeader.h cannot be included from .h files"); \
  }

#define CLOSE_BIG_HEADER \
  } VALIDATE_BIG_HEADER

#endif
// note this comes AFTER the final #endif
// It's cheap insurance against forgetting to type CLOSE_BIG_HEADER 

namespace bigheader_cheap_insurance {

Your users must do this:

#include <BigHeader.h>
CLOSE_BIG_HEADER

If they forget CLOSE_BIG_HEADER, their code will most probably not compile. If CLOSE_BIG_HEADER occurs in a file with an .h suffix, a static assertion is triggered.

Of course a malicious developer can redefine CLOSE_BIG_HEADER and abuse it with reckless abandon, but I don't think it is possible to protect against that.

I would not recommend such things in production code.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243