ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C++编程中__if_exists与__if_not_exists语句的用法

C++编程中__if_exists与__if_not_exists语句的用法 前言开门见山__if_exists与__if_not_exists是 MSVC 专有的编译器扩展不是标准 C。它们在 MSVC 文档里归在「Microsoft 专用」Microsoft-specific一节GCC 与 Clang 完全不认识这两个名字代码一旦出现在跨平台头文件里就会立刻编译失败。本文所有涉及它们的示例都只能在 MSVC 下编译。它们解决的问题很具体在编译期判断一个名字类型、类成员、函数、枚举量是否存在存在才编译这个语句块。写成语法就是__if_exists ( 名字 ) { 语句块 } __if_not_exists ( 名字 ) { 语句块 }两个最常见的误解。第一把它当成#ifdef的替代品——它们看着像、用着像但工作在完全不同的阶段#ifdef在预处理阶段对宏做文本级判断__if_exists在语义分析阶段对C 名字做存在性判断因此后者能检测「某个类有没有这个成员函数」而前者永远做不到。第二把它当成模板元编程的替代品——在模板里检测依赖名dependent name时它的行为是「检测模板定义点可见的名字」而不是「检测实例化时实参类型具备的成员」这类需求应该交给 SFINAE。本文讲清三件事它的语法与语义边界、它和#ifdef的实质差别、以及在必须跨平台时应该用什么替代它。一、语法、语义与边界__if_exists检查的「名字」可以是类或结构体的数据成员、成员函数、嵌套类型、全局函数、枚举量也可以是带命名空间或嵌套限定的名字。判断发生在编译期不产生的运行期分支也不产生任何运行期开销。// 仅限 MSVC // 编译方式cl /EHsc /std:c17 if_exists_demo.cpp #include iostream struct Widget { void Draw() { std::cout Widget::Draw\n; } int width 0; }; struct Gadget { void Resize(int) { std::cout Gadget::Resize\n; } }; int main() { Widget w; __if_exists(Widget::Draw) { // 存在这一块会被编译 w.Draw(); } __if_exists(Widget::Resize) { // 不存在整块被跳过不报错 w.Resize(1); } __if_not_exists(Widget::height) { // 不存在这一块会被编译 w.width 10; std::cout no height, width w.width \n; } __if_not_exists(Widget::Draw) { // 存在所以整块被跳过 std::cout unreachable\n; } return 0; }输出只有两行Widget::Draw与no height, width 10。四处判断里有三处按「名字是否存在」被保留或丢弃编译器不会对不存在的Widget::Resize报错。边界要记清楚它是语句不是预处理指令。因此宏已经被展开过了——用__if_exists去检测一个宏名是行不通的宏名在预处理阶段就没了。块内仍然要满足语法要求但按官方文档的说明语义分析会被跳过。也就是说块里的语句得能通过词法与语法解析但名字查找、重载解析、类型检查在做「名字不存在」的判断时不会施加到这一块上。这意味着块内可以写出「现在编不过」的代码而安然无事——这既是它的能力也是它的风险后面坑点里展开。具体边界请以 MSVC 文档与手头编译器版本的实测为准。它不改变重载解析也不引入新的作用域。二、和 #ifdef 的实质差别维度#ifdef/#if defined__if_exists/__if_not_exists工作阶段预处理语义分析判断对象宏名C 名字类型、成员、函数、枚举量能判断类成员吗不能能认识重载、using、访问控制吗不认识纯文本按语义规则处理这些名字标准出身C/C 标准MSVC 专有扩展非标准GCC / Clang支持❌ 不支持语法错误典型用途条件编译平台宏、版本宏MSVC 下检测第三方类型/成员是否存在差别用一个例子就能看得很清楚。「某个类型有没有Resize成员」这个问题#ifdef根本没有能力回答——它只能看到一行行的文本看不到类型和成员。而在 MSVC 里__if_exists能给出答案。它最正当的用途就是写兼容层某个第三方库在 1.2 版里给类加了SetTimeout成员你的代码要同时支持 1.1 和 1.2就可以这样写// 仅限 MSVC适配不同版本的第三方接口 #include third_party.h void Configure(third_party::Client c) { __if_exists(third_party::Client::SetTimeout) { c.SetTimeout(3000); // 1.2 及以上才编译这一行 } __if_not_exists(third_party::Client::SetTimeout) { c.SetRetryCount(3); // 老版本走这条路 } }同一条需求如果只用预处理宏就得依赖库里提供了一个版本宏THIRD_PARTY_VERSION 10200而很多时候它没有。三、在模板里用它以及可移植的替代方案把__if_exists用在模板里检测依赖名时要格外小心它判断的是模板定义点可见的名字而不是实例化时实参类型所具备的成员。也就是说它并不能用来实现「检测类型参数 T 有没有某个成员函数」这种需求。MSVC 在这方面的具体行为请以官方文档与你在用的编译器版本实测为准稳妥的做法是不要这么用。「按类型参数检测成员」是 SFINAESubstitution Failure Is Not An Error替换失败并非错误的地盘。C17 的标准写法是std::void_t检测惯用法// 可移植需要 C17std::void_t 是 C17 引入的 #include type_traits #include utility struct Widget { void Draw() {} int width 0; }; struct Gadget { void Resize(int) {} }; template class T, class void struct has_draw : std::false_type {}; template class T struct has_drawT, std::void_tdecltype(std::declvalT().Draw()) : std::true_type {}; static_assert(has_drawWidget::value, ); static_assert(!has_drawGadget::value, );配合 C17 的if constexpr写法上和__if_exists几乎一样直观而且完全可移植#include iostream // 接上面的 has_draw template class T void Render(T obj) { if constexpr (has_drawT::value) { obj.Draw(); // 条件为假时这个分支不会被实例化 } else { std::cout no Draw member\n; } } int main() { Widget w; Gadget g; Render(w); // 走 obj.Draw() 分支 Render(g); // 打印 no Draw member return 0; }if constexpr与__if_exists的关键区别在于if constexpr的丢弃分支仍然要被解析只是不被实例化而它判断的条件是已知的编译期常量所以可以依赖模板参数。这正是「检测成员」需要的能力。C20 起还可以用概念concept把这件事写得更短但那已经超出 C17 的基线此处不再展开。常见坑点1. 把__if_exists直接写进跨平台头文件// ❌ GCC/Clang 不认识这个语句头文件一被包含就编译失败 __if_exists(Foo::Bar) { /* ... */ } // ✅ 用编译器宏隔离非 MSVC 走另一条路径 #ifdef _MSC_VER __if_exists(Foo::Bar) { /* MSVC 路径 */ } #else /* 可移植路径SFINAE 或版本宏 */ #endif2. 用它去检测一个宏是否存在// ❌ 宏在预处理阶段已经被展开或替换掉了这里检测不到宏名 __if_exists(MY_FEATURE_MACRO) { /* ... */ } // ✅ 判断宏存在性只能用预处理器 #ifdef MY_FEATURE_MACRO /* ... */ #endif3. 名字写错一个字母整块静默消失// ❌ 把 SetTimeout 写成 SetTimeOut这块永远不会被编译功能莫名其妙没了 // 而且编译器一句话都不报 __if_exists(Client::SetTimeOut) { c.SetTimeout(3000); } // ✅ 写完后用 MSVC 的 /W4 加断点确认路径或临时改成无条件编译验证一次 c.SetTimeout(3000);这是这套扩展最阴的一类 bug错的方向反了编译器不但不报错还会因为「名字不存在」而合法地跳过整块代码。4. 在模板里用依赖名做检测// ❌ 模板里检测 T::Draw判断的是定义点的名字不是实参类型具备的成员 template class T void Render(T t) { __if_exists(T::Draw) { t.Draw(); } } // ✅ 可移植的 SFINAE if constexpr template class T void Render(T t) { if constexpr (has_drawT::value) { t.Draw(); } }5. 以为它会产生运行期分支或开销// ❌ 误以为运行期还要判断一次这个成员在不在 __if_exists(Foo::Bar) { /* 编译期就决定了既没有分支也没有开销 */ } // ✅ 要运行期可配置的行为老老实实用虚函数或 std::function6. 忘了块内声明的作用域只在块内// ❌ tmp 的作用域只在这个块里块外不可见 __if_exists(Foo::Bar) { int tmp 1; } // std::cout tmp; // 编译错误 // ✅ 需要跨块使用时在块外先声明 int tmp 0; __if_exists(Foo::Bar) { tmp 1; }7. 块内的代码长期不被编译条件一成立才发现它早就编不过了// ❌ 这段代码在 1.1 版库下从没被编译过升级到 1.2 的当天才暴露错误 __if_exists(Client::SetTimeout) { c.SetTimeout(3000); // 参数类型写错了但一直没人发现 } // ✅ 每升一次依赖版本把兼容分支都走一遍构建总结要点结论出身MSVC 专有扩展非标准GCC/Clang 不支持工作阶段语义分析编译期不是预处理判断对象类型、类成员、函数、枚举量等 C 名字判断宏做不到宏在预处理阶段已处理完运行期开销无整块代码要么被编译要么被跳过检测模板成员不合适应改用std::void_tif constexprC17或概念C20最正当的用途MSVC 下针对第三方库版本差异写兼容分支__if_exists与__if_not_exists是一把好用的窄刃刀在 MSVC 上做「这个成员存不存在」的兼容分支时它比版本宏更直接、比 SFINAE 更好读。但它的两个特性必须同时记住——非标准所以要隔离在_MSC_VER里块内语义不检查所以名字打错它也不会告诉你只会默默把那块代码删掉。真正需要可移植的成员检测时std::void_t加if constexpr才是正确的那条路。
返回列表