Workarounds for using __has_include when it may or may not be defined - Is it valid to pass __has_include as a macro argument?

12 hours ago 2
ARTICLE AD BOX

Often, I'd like to chain preprocessor conditionals involving __has_include (defined in C++17 and C23, and supported in earlier versions as an extension by many compilers). A simple example looks something like this:

#if defined(__has_include) && __has_include("header.h") #include "header.h" #else #include "backup-header.h" #endif

The problem is that this fails to compile for compilers/standards that don't support __has_include. The defined(__has_include) check doesn't help us because the phrase __has_include("header.h") is not syntactically valid in the first place if __has_include is not defined.

GCC's documentation suggests something like the following:

#if defined __has_include # if __has_include (<stdatomic.h>) # include <stdatomic.h> # endif #endif

https://gcc.gnu.org/onlinedocs/cpp/_005f_005fhas_005finclude.html

The problem with this is that it becomes difficult to chain the #else branch. If we were to adapt it to our example, we need to define an additional macro to track whether the __has_include succeeded, which makes the code much more verbose:

#ifdef __has_include #if __has_include("header.h") #include "header.h" #define HAS_INCLUDE_HEADER_H #endif #endif #ifndef HAS_INCLUDE_HEADER_H #include "backup-header.h" #endif #undef HAS_INCLUDE_HEADER_H

If we don't want to do that, a naive solution would be to add another layer of indirection:

// This part can be done once in a common header #ifdef __has_include #define MY_HAS_INCLUDE(Arg) __has_include(Arg) #else #define MY_HAS_INCLUDE(Arg) 0 #endif #if MY_HAS_INCLUDE("header.h") #include "header.h" #else #include "backup-header.h" #endif

This does work on many (all?) compilers, but unfortunately it isn't standard-compliant, and recent Clang will warn about it. The relevant passage from the standard is as follows:

The expression that controls conditional inclusion shall be an integral constant expression except that identifiers (including those lexically identical to keywords) are interpreted as described below and it may contain zero or more defined-macro-expressions and/or has-include-expressions and/or has-attribute-expressions as unary operator expressions.

...

The #ifdef, #ifndef, #elifdef, and #elifndef directives, and the defined conditional inclusion operator, shall treat __has_include and __has_cpp_attribute as if they were the names of defined macros. The identifiers __has_include and __has_cpp_attribute shall not appear in any context not mentioned in this subclause.

https://timsong-cpp.github.io/cppwp/n4950/cpp.cond

(C23 has an essentially identical clause)

My understanding is that this is saying that "__has_include can only be used in the controlling expression of an #if/#elif directive or as the argument to defined", but I'm a little fuzzy on the exact limits.


Now, to the meat of my question. I was thinking through a couple workarounds, and has wondering whether any of them are standard-compliant and would work.

(A) Defining MY_HAS_INCLUDE as a macro without arguments instead of one that takes an argument silences Clang's warning, but I doubt this is actually valid since we're still writing the phrase __has_include outside of an #if condition. Is there any chance this is standard-compliant?

// This part can be done once in a common header #ifdef __has_include #define MY_HAS_INCLUDE __has_include #else #define HAS_INCLUDE_STUB(Arg) 0 #define MY_HAS_INCLUDE HAS_INCLUDE_STUB #endif #if MY_HAS_INCLUDE("header.h") #include "header.h" #else #include "backup-header.h" #endif

(B) My most promising idea is this. Would wrapping a macro around the entire __has_include expression like this be valid?

// This part can be done once in a common header #ifdef __has_include #define IF_HAS_INCLUDE_SUPPORTED(Arg) Arg #else #define IF_HAS_INCLUDE_SUPPORTED(Arg) 0 #endif #if IF_HAS_INCLUDE_SUPPORTED(__has_include("header.h")) #include "header.h" #else #include "backup-header.h" #endif

(C) If it isn't valid for __has_include to be passed to a macro at all, would something like this be valid? In this case the __has_include phrase is only passed as the argument to a macro in the case where it isn't defined. (Although this is so ugly that it's probably not worth actually doing)

// This part can be done once in a common header #ifdef __has_include #define HAS_INCLUDE_TAKE_EMPTY_PAREN() #define PRE_HAS_INCLUDE #define POST_HAS_INCLUDE HAS_INCLUDE_TAKE_EMPTY_PAREN ( #else #define HAS_INCLUDE_CONSUMER(Arg) 0 #define PRE_HAS_INCLUDE HAS_INCLUDE_CONSUMER ( #define POST_HAS_INCLUDE #endif #if PRE_HAS_INCLUDE __has_include("header.h") POST_HAS_INCLUDE ) #include "header.h" #else #include "backup-header.h" #endif

EDIT: (D) A better version combining the above two workarounds. In this case IF_HAS_INCLUDE_SUPPORTED(__has_include("header.h")) either expands to 0 if __has_include is not supported, or (__has_include("header.h")) in parens if it is. And in the case where __has_include is supported, it's never passed as the argument to a macro. Is this valid? Surely it must be.

// This part can be done once in a common header #ifdef __has_include #define IF_HAS_INCLUDE_SUPPORTED #else #define IF_HAS_INCLUDE_SUPPORTED(Arg) 0 #endif #if IF_HAS_INCLUDE_SUPPORTED(__has_include("header.h")) #include "header.h" #else #include "backup-header.h" #endif

Addendum: Something that I've often seen done is defining __has_include itself if it isn't defined:

// This part can be done once in a common header #ifndef __has_include #define __has_include(Arg) 0 #endif #if PRE_HAS_INCLUDE __has_include("header.h") POST_HAS_INCLUDE ) #include "header.h" #else #include "backup-header.h" #endif

This is still technically a violation of the standard since names beginning with double underscores are reserved. I also don't like it because it makes it difficult to see what's actually going on (since someone reading later code might not realize that we defined __has_include ourselves), and it breaks the ability of later code to check whether __has_include is actually supported using defined.

Read Entire Article