26.More C++ Idioms - enable_if惯用法 and more

enter image description here

继续讨论more C++ idioms惯用法,这个wikibook,今天讨论enable_if惯用法, 这个惯用法的意图是Allow function overloading based on arbitrary properties of type


ChatGPT 说: std::enable_if 是 C++ 中的一种 SFINAE(Substitution Failure Is Not An Error)技术,主要用于根据类型的属性选择合适的重载版本。其意图是基于类型的某些特性(比如是否满足某个 trait,是否是某种类型的派生类等)来启用或禁用函数模板的某些特化版本。

std::enable_if 的基本用法

std::enable_if 定义在 头文件中,其基本形式如下:


template<bool B, class T = void> struct enable_if {}; template<class T> struct enable_if<true, T> { using type = T; }; template<bool B, class T = void> using enable_if_t = typename enable_if<B, T>::type;

它的作用是:

  • 当 B 为 true 时,enable_if<B, T>::type 被定义为 T,使得相关代码可用。
  • 当 B 为 false 时,enable_if<B, T>::type 不存在,从而触发 SFINAE,导致该函数或类模板的某个特化版本被排除。

示例 1:基于类型属性选择函数

假设我们想要让 print() 仅适用于整数类型:


#include <iostream> #include <type_traits> // 仅当 T 为整数类型时,启用这个 print 版本 template <typename T> typename std::enable_if<std::is_integral<T>::value>::type print(T value) { std::cout << "Integer: " << value << '\n'; } // 仅当 T 为浮点数类型时,启用这个 print 版本 template <typename T> typename std::enable_if<std::is_floating_point<T>::value>::type print(T value) { std::cout << "Floating point: " << value << '\n'; } int main() { print(42); // 调用整数版本 print(3.14); // 调用浮点数版本 // print("Hello"); // 编译错误,因为 std::is_integral<const char*> 为 false }

解析 std::enable_if<std::is_integral<T>::value> 仅当 T 是整数类型时才会被定义。 std::enable_if<std::is_floating_point<T>::value> 仅当 T 是浮点数类型时才会被定义。 这样就实现了基于类型属性的函数重载。

示例 2:用于类的特化

如果我们想要限制一个类的实例化,只允许 T 为整数类型:


#include <iostream> #include <type_traits> // 仅当 T 为整数类型时才允许实例化这个类 template <typename T, typename Enable = void> class MyClass; // 仅当 T 是整数类型时,MyClass<T> 被定义 template <typename T> class MyClass<T, typename std::enable_if<std::is_integral<T>::value>::type> { public: void show() { std::cout << "MyClass instantiated with integral type\n"; } }; int main() { MyClass<int> obj; // OK obj.show(); // MyClass<double> obj2; // 编译错误,double 不是整数类型 }

解析 MyClass<T> 的泛化版本没有定义,除非 T 是整数类型。 这是一种静态约束(Static Constraint),可以有效防止用户误用某个类模板。

示例 3:在类成员函数中使用 std::enable_if

有时候,我们希望同一个类中的某些成员函数仅在某些条件下可用:


#include <iostream> #include <type_traits> class Example { public: // 仅当 T 为整数类型时,启用这个成员函数 template <typename T> typename std::enable_if<std::is_integral<T>::value>::type process(T value) { std::cout << "Processing integer: " << value << '\n'; } // 仅当 T 为浮点数类型时,启用这个成员函数 template <typename T> typename std::enable_if<std::is_floating_point<T>::value>::type process(T value) { std::cout << "Processing floating point: " << value << '\n'; } }; int main() { Example e; e.process(42); // 调用整数版本 e.process(3.14); // 调用浮点版本 // e.process("Hello"); // 编译错误,std::is_integral<const char*> 为 false }

解析 process() 只有在 T 为整数或浮点数类型时才会被实例化,避免了不必要的编译错误。

示例 4:std::enable_if_t 简化语法

在 C++14 及以上版本,我们可以使用 std::enable_if_t 让代码更加简洁:


#include <iostream> #include <type_traits> template <typename T> std::enable_if_t<std::is_integral_v<T>> print(T value) { std::cout << "Integer: " << value << '\n'; } template <typename T> std::enable_if_t<std::is_floating_point_v<T>> print(T value) { std::cout << "Floating point: " << value << '\n'; } int main() { print(42); print(3.14); }

解析 std::enable_if_t<B> 直接返回 type,无需写 typename std::enable_if<B>::type,减少模板代码的复杂度。

总结

std::enable_if 主要用于:

  • 函数重载选择(基于 std::is_integral,std::is_floating_point 等 trait)。
  • 模板类的 SFINAE 约束(防止无意义的模板实例化)。
  • 限制类成员函数的可用性(避免某些类型触发不必要的实例化)。 C++14 之后可以使用 std::enable_if_t<> 简化语法。 在 C++17 之后,std::enable_if 通常可以用 if constexpr 代替,但在 C++11/C++14 代码中仍然是非常重要的技术。

您说: C++20 requires 干的工作和这个类似?


ChatGPT 说: 是的,C++20 引入的 requires 关键字和 std::enable_if 在某些方面具有类似的作用,但 requires 提供了更直观和可读性更好的语法,用于约束模板参数。

std::enable_if vs. requires

  • std::enable_if 使用 SFINAE(Substitution Failure Is Not An Error) 机制来选择合适的模板实例化版本。
  • requires 结合概念(Concepts) 提供了一种 更直观的方式来表达类型约束,并且错误信息更清晰。

示例 1:基于类型属性的函数重载

使用 std::enable_if


#include <iostream> #include <type_traits> template <typename T> typename std::enable_if<std::is_integral_v<T>>::type print(T value) { std::cout << "Integer: " << value << '\n'; } template <typename T> typename std::enable_if<std::is_floating_point_v<T>>::type print(T value) { std::cout << "Floating point: " << value << '\n'; } int main() { print(42); // 调用整数版本 print(3.14); // 调用浮点版本 }

std::enable_if<std::is_integral_v<T>> 使得 print() 仅对整数类型有效。 代码较为繁琐,可读性较差。 使用 requires

#include <iostream>
#include <concepts> // C++20 新增

template <std::integral T>
void print(T value) {
    std::cout << "Integer: " << value << '\n';
}

template <std::floating_point T>
void print(T value) {
    std::cout << "Floating point: " << value << '\n';
}

int main() {
    print(42);    // 调用整数版本
    print(3.14);  // 调用浮点版本
}

std::integralstd::floating_point 是 C++20 提供的标准概念(Concepts)。 代码更简洁,逻辑更加直观。

示例 2:约束类模板

使用 std::enable_if


#include <iostream> #include <type_traits> template <typename T, typename Enable = void> class MyClass; // 仅当 T 是整数类型时,MyClass<T> 被定义 template <typename T> class MyClass<T, typename std::enable_if<std::is_integral_v<T>>::type> { public: void show() { std::cout << "MyClass instantiated with integral type\n"; } }; int main() { MyClass<int> obj; // OK obj.show(); // MyClass<double> obj2; // 编译错误 }

使用 requires


#include <iostream> #include <concepts> template <std::integral T> class MyClass { public: void show() { std::cout << "MyClass instantiated with integral type\n"; } }; int main() { MyClass<int> obj; // OK obj.show(); // MyClass<double> obj2; // 编译错误 }

requires 使得 模板参数约束 更直观,错误信息也更清晰。

示例 3:自定义概念

C++20 允许我们自定义概念(Concepts),比 std::enable_if 更易读:


#include <iostream> #include <concepts> // 自定义概念,检查 T 是否可被转换为 int template <typename T> concept ConvertibleToInt = std::convertible_to<T, int>; template <ConvertibleToInt T> void func(T value) { std::cout << "Convertible to int: " << value << '\n'; } int main() { func(10); // OK func(3.14); // OK (double 可转换为 int) // func("hello"); // 编译错误 }

concept ConvertibleToInt 让 func() 仅适用于 可以转换为 int 的类型。 requires 提供了一种声明式的约束表达方式,不依赖 SFINAE。

总结

|特性 | std::enable_if |requires + Concepts| |--|--|--| | 语法复杂度 | 复杂(嵌套 typename) |简洁直观| |错误信息 | 晦涩难懂 |友好且清晰| | 适用范围 | 可用于类/函数模板 |可用于类/函数/变量模板| C++ 版本 C++11/C++14/C++17 C++20 及以上

结论:

  • C++11/14:必须使用 std::enable_if 进行模板特化或 SFINAE 限制。
  • C++20:推荐使用 requires + Concepts,语法更优雅,错误信息更清晰。 所以,requires 在 C++20 之后逐渐取代 std::enable_if,成为更现代的类型约束方式。

您说: // 仅当 T 为整数类型时才允许实例化这个类 template class MyClass; 这个模板语法我还不太熟悉,给我介绍一下?


ChatGPT 说: 这段代码使用了 SFINAE(Substitution Failure Is Not An Error) 和 std::enable_if 来 限制模板的实例化。它的作用是 仅允许 T 为整数类型时才能实例化 MyClass。

1. 代码解析


template <typename T, typename Enable = void> class MyClass;

T 是 主模板 的泛型参数。 Enable = void 是 第二个模板参数,默认值是 void,它通常用于 SFINAE 机制 进行启用或禁用模板特化。

2. enable_if 限制实例化

通常,我们会结合 std::enable_if 来 控制哪些类型可以实例化 MyClass:


#include <iostream> #include <type_traits> // 仅当 T 是整数类型(integral)时,才允许实例化 MyClass template <typename T, typename Enable = std::enable_if_t<std::is_integral_v<T>>> class MyClass { public: void print() { std::cout << "Integer type: " << typeid(T).name() << std::endl; } }; int main() { MyClass<int> obj; // ✅ int 是整数类型,可以实例化 obj.print(); // MyClass<double> obj2; // ❌ 编译错误,double 不是整数类型 return 0; }

📌 关键点解析 std::enable_if_t<条件> 作用

std::enable_if_t<std::is_integral_v<T>>

std::enable_if_t<条件> 仅当 条件 为 true 时,才定义 Enable 为 void。 如果 条件 为 false,则 Enable 没有定义,导致 MyClass<T> 无法实例化(SFINAE)。 为什么 Enable = void?

  • Enable 默认是 void,这样 std::enable_if_t 时 Enable 仍然是 void,不会影响类定义。
  • Enable 不是 void 时,匹配 MyClass 的时候就会失败,从而防止错误类型实例化。

3. enable_if 变体

(1)支持整数和浮点数的不同实现 如果我们希望整数类型和浮点数类型都有不同的实现,可以这样做:


#include <iostream> #include <type_traits> // 整数类型特化 template <typename T, typename Enable = std::enable_if_t<std::is_integral_v<T>>> class MyClass { public: void print() { std::cout << "Integer type: " << typeid(T).name() << std::endl; } }; // 浮点数类型特化 template <typename T> class MyClass<T, std::enable_if_t<std::is_floating_point_v<T>>> { public: void print() { std::cout << "Floating point type: " << typeid(T).name() << std::endl; } }; int main() { MyClass<int> obj1; obj1.print(); // 输出:Integer type: int MyClass<double> obj2; obj2.print(); // 输出:Floating point type: double return 0; }

(2)C++20 requires 替代 enable_if C++20 引入了 concepts,可以用 requires 代替 std::enable_if:


#include <iostream> #include <concepts> // 仅当 T 是整数类型时,允许实例化 template <std::integral T> class MyClass { public: void print() { std::cout << "Integer type: " << typeid(T).name() << std::endl; } }; int main() { MyClass<int> obj; obj.print(); // ✅ int 是整数类型,编译成功 // MyClass<double> obj2; // ❌ double 不是整数类型,编译错误 return 0; }

✅ C++20 的 requires 语法比 enable_if 更直观,也更易读!

总结

方式 适用情况 代码可读性 兼容性
std::enable_if C++11 及以上 一般 ✅ 适用于所有现代 C++
requires (Concepts) C++20 及以上 很好 ❌ 仅 C++20 及以上

🔹 结论:

  • C++11/14/17:使用 std::enable_if 进行 SFINAE 约束。
  • C++20 及以上:使用 requires 更加 简洁优雅,避免 enable_if 复杂语法。 🚀 建议: 如果你的项目支持 C++20,推荐使用 requires,否则可以继续使用 enable_if。

您说: 仅仅从这个template class MyClass; 应该看不出来T 是整型才做任何的特化吧?


ChatGPT 说: 是的,从这一行代码本身:

template <typename T, typename Enable = void>
class MyClass;

无法直接看出 T 需要是整型。它只是一个主模板声明,定义了 MyClass 这个模板类,但没有提供具体实现。

关键点:

  • Enable = void 只是提供了一个默认类型参数,并没有施加任何约束。
  • 这一行代码本身不包含 enable_if 逻辑,所以 T 可以是任何类型,并不会限制 T 只能是整数类型。
  • 要施加整数类型约束,需要结合 std::enable_if
  • 通常,我们会在主模板或特化版本中添加 std::enable_if,来实现 SFINAE 机制,比如:

方法 1:在主模板里使用 enable_if


#include <iostream> #include <type_traits> // 仅允许 `T` 是整数类型时实例化 template <typename T, typename Enable = std::enable_if_t<std::is_integral_v<T>>> class MyClass { public: void print() { std::cout << "Integer type: " << typeid(T).name() << std::endl; } }; int main() { MyClass<int> obj; // ✅ int 是整数类型,可以实例化 obj.print(); // MyClass<double> obj2; // ❌ double 不是整数类型,编译失败 return 0; }

解析:

Enable = std::enable_if_t<std::is_integral_v<T>>

当 T 是整数类型时,std::enable_if_t<true> 解析为 void,因此 Enable = void,匹配默认参数,MyClass<T> 可以实例化。 当 T 不是整数类型时,std::enable_if_t<false> 无效,导致 SFINAE(Substitution Failure Is Not An Error)排除该模板实例化。

方法 2:使用特化

另一种方式是定义一个没有实现的主模板,然后用 enable_if 进行特化:


#include <iostream> #include <type_traits> // 声明主模板(不提供实现,防止非法实例化) template <typename T, typename Enable = void> class MyClass; // 仅当 T 是整数类型时,才提供 MyClass 的特化版本 template <typename T> class MyClass<T, std::enable_if_t<std::is_integral_v<T>>> { public: void print() { std::cout << "Integer type: " << typeid(T).name() << std::endl; } }; int main() { MyClass<int> obj; // ✅ int 是整数类型,匹配特化版本 obj.print(); // MyClass<double> obj2; // ❌ double 没有匹配的 MyClass 特化,编译错误 return 0; }

解析:

主模板 MyClass<T, Enable> 只是声明,没有实现,默认情况下无法实例化。 特化版本 MyClass<T, std::enable_if_t<std::is_integral_v<T>>> 仅当 T 是整数类型时,std::enable_if_t<true> 解析为 void,与 Enable = void 匹配,此时 MyClass 可以实例化。 如果 T 不是整数类型,std::enable_if_t<false> 无效,不会匹配任何特化,导致编译错误。 结论 从 template <typename T, typename Enable = void> class MyClass; 本身 看不出来 T 是否是整数类型,它只是一个普通的模板声明。 没有 enable_if 逻辑,T 可以是任何类型,不会限制 T 只能是整数。 要施加整数约束

方法 1(在主模板中用 enable_if):


template <typename T, typename Enable = std::enable_if_t<std::is_integral_v<T>>> class MyClass { ... };

适合简单的模板约束。 但 Enable 默认值会影响代码可读性。

方法 2(使用模板特化):


template <typename T, typename Enable = void> class MyClass; // 声明但不实现 template <typename T> class MyClass<T, std::enable_if_t<std::is_integral_v<T>>> { ... };

适合复杂的模板特化。 主模板防止无效实例化,特化版本控制适用类型。 ✅ 如果 C++20 可用,推荐用 requires 取代 enable_if:


template <std::integral T> class MyClass { ... };

这样写法更清晰易读! 🚀

您说: 所以 template class MyClass; 这个只是模板声明? 定义了模板类 MyClass,但是没有具体的实现? 这个ok吗? 如果没有具体其他实现,如何是实例化这个模板类?


ChatGPT 说: 是的,这只是一个模板声明,它定义了 MyClass 这个模板类,但没有提供具体实现,所以不能直接实例化。

分析 template <typename T, typename Enable = void> class MyClass;


template <typename T, typename Enable = void> class MyClass;

这是一个不完整的模板声明,它告诉编译器 "有一个模板类 MyClass,它接受两个模板参数 T 和 Enable"。 但是没有提供类的实现,所以这个类是不完整的(incomplete type)。 不能直接实例化,否则会导致编译错误。 如果没有实现,能否实例化? 不可以! 如果你尝试直接实例化 MyClass,编译器会报错:


int main() { MyClass<int> obj; // ❌ 错误:incomplete type }

编译器错误


error: invalid use of incomplete type 'class MyClass<int>'

因为 MyClass<int> 只是被声明了,但没有定义其成员变量、成员函数等,编译器无法实例化它。

如何使它可实例化? 你需要提供至少一个实现,比如:

方法 1:提供完整的主模板


#include <iostream> template <typename T, typename Enable = void> class MyClass { public: void print() { std::cout << "MyClass instantiated with type: " << typeid(T).name() << std::endl; } }; int main() { MyClass<int> obj; // ✅ 现在可以实例化 obj.print(); }

✅ 现在 MyClass<int> 可以实例化了!

方法 2:使用特化

如果你想限制 MyClass 只适用于某些类型(比如 T 只能是整数类型),你可以提供特化版本:


#include <iostream> #include <type_traits> // 声明主模板(没有实现) template <typename T, typename Enable = void> class MyClass; // 仅当 T 是整数类型时,才提供特化版本 template <typename T> class MyClass<T, std::enable_if_t<std::is_integral_v<T>>> { public: void print() { std::cout << "Integer type: " << typeid(T).name() << std::endl; } }; int main() { MyClass<int> obj; // ✅ 只允许整数类型 obj.print(); // MyClass<double> obj2; // ❌ 编译错误 }

✅ 只有整数类型才会匹配特化版本! 如果 T 不是整数,比如 double,编译器不会找到 MyClass 的实现,导致错误。

结论

template <typename T, typename Enable = void> class MyClass; 只是声明,没有实现,不能直接实例化。 要实例化 MyClass<T>,必须提供具体实现,可以是: - 完整的主模板 - 使用 enable_if 进行特化 - 如果你不提供任何实现,尝试 MyClass obj; 会导致编译错误。 ✅ 如果 C++20 可用,推荐使用 requires 语法,避免 enable_if 复杂的模板参数:


#include <iostream> #include <concepts> template <std::integral T> class MyClass { public: void print() { std::cout << "Integer type: " << typeid(T).name() << std::endl; } }; int main() { MyClass<int> obj; // ✅ 只允许整数类型 obj.print(); // MyClass<double> obj2; // ❌ 编译错误 }

这样代码更简洁可读!🚀

您说: // 声明主模板(没有实现) template class MyClass; 所以这个主模板声明,貌似只是在编译阶段 供编译器在进行模板示例检查提供一个check?


ChatGPT 说: 是的,template class MyClass; 只是一个声明(declaration),它的主要作用是告诉编译器有这样一个模板类,但并没有提供实现。

它的作用是什么? 提供一个检查点(check)

  • 当代码中使用 MyClass<T> 时,编译器会检查 MyClass<T> 是否可用(是否有实现)。 如果没有对应的实现,而你尝试实例化它,就会报编译错误。
  • 允许特化

这个主模板的存在可以允许后续的模板特化,比如:


template <typename T, typename Enable = void> class MyClass; // 仅仅声明,无法实例化 // 只有整数类型的 MyClass 才有实现 template <typename T> class MyClass<T, std::enable_if_t<std::is_integral_v<T>>> { public: void print() { std::cout << "Integer type: " << typeid(T).name() << std::endl; } }; int main() { MyClass<int> obj; // ✅ 允许 obj.print(); // MyClass<double> obj2; // ❌ 错误,主模板无实现,特化不匹配 }

这样,即使你在代码中写了 MyClass<double>,它不会去实例化 MyClass<T, void>,因为 MyClass 没有对应的实现,从而保证了编译时的静态检查。 所以它的作用主要是: ✅ 让编译器知道 MyClass 是一个模板,但默认不能实例化。 ✅ 为后续的特化提供一个基础,使 enable_if 之类的技术可以正确生效。 ✅ 避免某些情况下的编译错误,当特化的 MyClass 没有定义时,编译器可以报错,而不是尝试匹配一个默认实现。

但是,如果你不打算使用特化,而是希望 MyClass 总是有一个实现,你应该提供主模板的完整定义,而不是只声明它! 🚀

理解了,不完整的模板类声明还是配合特化一起使用的。否则需要定义完整的主模板类


ChatGPT 说: 是的,不完整的模板类声明主要是为了配合特化使用,如果没有特化,那就需要定义完整的主模板,否则这个类是无法被实例化的!💡


总结关键点

✅ 不完整的模板声明 (template class MyClass;) 主要用于:

配合特化(如 enable_if 限制某些类型)。 让编译器知道 MyClass 是一个模板,但默认不能实例化,防止意外使用。 ✅ 如果不使用特化,就应该提供完整的主模板定义


template <typename T> class MyClass { public: void print() { std::cout << "Generic MyClass: " << typeid(T).name() << std::endl; } };

✅ 如果只声明而没有实现,尝试实例化会报错


template <typename T, typename Enable = void> class MyClass; // 不完整的声明 int main() { MyClass<int> obj; // ❌ 编译错误,无法实例化 }

💡

最佳实践: 如果你只是想创建一个泛型类,不要只写一个不完整的声明,而是直接定义主模板; 如果你希望有特定的类型限制(如只允许整数类型),使用 enable_if 或 requires 进行特化。


你对这个理解已经很清楚了!🚀 继续加油! 💪

评论