C++ 17 __has_include预处理表达式

C++ 17 __has_include预处理表达式

__has_includeC++17 标准化的一个预处理特性,用来判断某个头文件是否存在。它特别适合做兼容性处理,比如“有这个头文件就包含,没有就走备用方案”。

1. 为什么需要它?

以前做跨平台或者兼容不同编译环境时,经常会遇到这样的问题:

  • 某些标准库头文件在旧环境里没有
  • 某些第三方库只在部分机器安装了
  • 同一功能在不同平台的头文件名字不一样

如果没有 __has_include,这类判断通常只能依赖构建系统或者手工配置。
有了它之后,预处理阶段就能直接判断头文件是否存在。

2. 基本语法

1
2
3
#if __has_include(<optional>)
#include <optional>
#endif

或者:

1
2
3
#if __has_include("my_config.h")
#include "my_config.h"
#endif

它既支持尖括号形式,也支持双引号形式。

3. 最简单的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

#if __has_include(<optional>)
#include <optional>
#define HAS_OPTIONAL 1
#else
#define HAS_OPTIONAL 0
#endif

int main()
{
std::cout << HAS_OPTIONAL << std::endl;
return 0;
}

如果当前编译环境支持 <optional>,就会输出 1

4. 常见使用场景

4.1 标准库兼容处理

1
2
3
4
5
6
7
#if __has_include(<filesystem>)
#include <filesystem>
namespace fs = std::filesystem;
#elif __has_include(<experimental/filesystem>)
#include <experimental/filesystem>
namespace fs = std::experimental::filesystem;
#endif

这种写法很常见,尤其是兼容不同标准库实现时。

4.2 第三方库是否存在

1
2
3
4
5
6
#if __has_include(<spdlog/spdlog.h>)
#include <spdlog/spdlog.h>
#define USE_SPDLOG 1
#else
#define USE_SPDLOG 0
#endif

4.3 平台差异处理

1
2
3
#if __has_include(<unistd.h>)
#include <unistd.h>
#endif

5. 和宏判断有什么区别?

有些人会把它和 #ifdef 混在一起,但它们判断的对象不一样:

  • #ifdef XXX:判断宏是否定义
  • __has_include(...):判断头文件是否能被包含

所以两者用途并不冲突。

6. 一个更完整的兼容示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

#if __has_include(<string_view>)
#include <string_view>
using TextView = std::string_view;
#else
#include <string>
using TextView = const std::string&;
#endif

int main()
{
std::cout << "ok" << std::endl;
return 0;
}

这个思路在写头文件库时会比较常见。

7. 使用时要注意的地方

7.1 只是判断“能不能包含”

__has_include 只能说明“这个头文件路径存在并且能被找到”,不代表库功能一定完整可用。

7.2 不能替代所有版本判断

有时候即使头文件存在,里面某些接口也可能不存在。
这种时候还要结合编译器版本、标准版本或者特性宏一起判断。

7.3 最好配合清晰的降级方案

如果你写了 #if __has_include(...),最好同时把 #else 里的备用逻辑也写清楚,而不是只判断不处理。

总结

__has_include 是一个非常实用的小工具,尤其适合处理不同编译环境下的头文件差异。
它不复杂,但在做库兼容、跨平台构建和新旧标准过渡时很有价值。对于现代 C++ 项目来说,这个特性还是挺值得记住的。