💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## enable_if ### 头文件: `"boost/utility/enable_if.hpp"` 有时候,我们希望控制某个函数或类模板的特化是否可以加入到重载决议时使用的重载或特化的集合中。例如,考虑一个重载的函数,它有一个版本是带一个`int`参数的普通函数,另一个版本是一个函数模板,它要求参数类型 `T` 具有一个名为`type`的嵌套类型。它们看起来可能象这样: ``` void some_func(int i) { std::cout << "void some_func(" << i << ")\n"; } template <typename T> void some_func(T t) { typename T::type variable_of_nested_type; std::cout << "template <typename T> void some_func(" << t << ")\n"; } ``` 现在,想象一下当你在代码中调用 `some_func` 将发生什么。如果参数的类型为 `int`, 第一个版本将被调用。如果参数的类型是 `int`以外的其它类型,则第二个(模板)版本将被调用。 这没问题,只要这个类型有一个名为`type`的嵌套类型,但如果它没有,这段代码就不能通过编译。这会是一个问题吗?好的,考虑一下如果你用其它整数类型来调用,如`short`, 或 `char`, 或 `unsigned long`,那么又会发生什么。 ``` #include <iostream> void some_func(int i) { std::cout << "void some_func(" << i << ")\n"; } template <typename T> void some_func(T t) { typename T::type variable_of_nested_type; std::cout << "template <typename T> void some_func(" << t << ")\n"; } int main() { int i=12; short s=12; some_func(i); some_func(s); } ``` 编译这段程序时,你将从失败的编译器中得到类似以下的输出: ``` enable_if_sample1.cpp: In function 'void some_func(T) [with T = short int]': enable_if_sample1.cpp:17: instantiated from here enable_if_sample1.cpp:8: error: 'short int' is not a class, struct, or union type Compilation exited abnormally with code 1 at Sat Mar 06 14:30:08 ``` 就是这样。`some_func` 的模板版本被选为最佳的重载,但这个版本中的代码对于类型`short`而言是无效的。我们怎样才能避免它呢?好的,我们希望仅对含有名为type的嵌套类型的类使用模板版本的 `some_func` ,而对于其它没有这个嵌套类型的类则忽略它。我们能够做到。最简单的方法,但不一定是实际中总能使用的方法,是把模板版本的返回类型改为如下: ``` template <typename T> typename T::type* some_func(T t) { typename T::type variable_of_nested_type; std::cout << "template <typename T> void some_func(" << t << ")\n"; return 0; } ``` 如果你没有学过 SFINAE (匹配失败不是错误),\[8\] 很可能现在你的脸上会有困惑的表情。编译修改过的代码,我们的例子会通过编译。`short` 被提升为 `int`, 并且第一个版本被调用。这种令人惊奇的行为的原因是模板版本的 `some_func` 不再包含在重载决议的集合内了。它被排除在内是因为,编译器看到了这个函数的返回类型要求模板类型`T` 要有一个嵌套类型`type` ,而它知道 `short` 不满足这个要求,所以它把这个函数模板从重载决议集合中删掉了。这就是 Daveed Vandevorde 和 Nicolai Josuttis 教给我们的 SFINAE, 它意味着宁可对有问题的类型不考虑函数的重载,也不要产生一个编译器错误。如果类型有一个符合条件的嵌套类型,那么它就是重载决议集合的一部分。 > \[8\] 见参考书目[3]。 ``` class some_class { public: typedef int type; }; int main() { int i=12; short s=12; some_func(i); some_func(s); some_func(some_class()); } ``` 运行该程序的输出如下: ``` void some_func(12) void some_func(12) template <typename T> void some_func(T t) ``` 这种办法可以用,但它不太好看。在这种情形下,我们可以不管原来的 `void` 返回类型,我们可以用其它类型替换它。但如果不是这种情形,我们就要给函数增加一个参数并给它指定一个缺省值。 ``` template <typename T> void some_func(T t,typename T::type* p=0) { typename T::type variable_of_nested_type; std::cout << "template <typename T> void some_func(T t)\n"; } ``` 这个版本也是使用 SFINAE 来让自己不会被无效类型所使用。这两种解决方案的问题都在于它们有点难看,我们把它们弄成了公开接口的一部分,并且它们只能在某些情形下使用。Boost 提供了一个更干净的解决方法,这种方法不仅在语法上更好看,而且提供了比前面的解决方法更多的功能。 ### 用法 要使用 `enable_if` 和 `disable_if`, 就要包含头文件 `"boost/utility/enable_if.hpp"`. 在第一个例子中,我们将禁止第二个版本的 `some_func` ,如果参数的类型是整型的话。象一个类型是否整型这样的类型信息可以用另一个Boost库`Boost.Type_traits`来取得。`enable_if` 和 `disable_if` 模板都通过接受一个谓词来控制是否启用或禁止一个函数。 ``` #include <iostream> #include "boost/utility/enable_if.hpp" #include "boost/type_traits.hpp" void some_func(int i) { std::cout << "void some_func(" << i << ")\n"; } template <typename T> void some_func( T t,typename boost::disable_if< boost::is_integral<T> >::type* p=0) { typename T::type variable_of_nested_type; std::cout << "template <typename T> void some_func(T t)\n"; } ``` 虽然这看起来与我们前面所做的差不多,但它表达了一些我们使用直接的方法所不能表达的东西,而且它在函数的声明中表达了关于这个函数的重要信息。看到这些,我们可以清楚的知道这个函数要求类型`T`不能是一个整数类型。如果我们希望仅对含有嵌套类型`type`的类型启用这个函数,它也可以做得更好,而且我们还可以用另一个库Boost.Mpl\[9\] 来做。如下: > \[9\] Boost.Mpl 超出了本书的范围。访问 [http://www.boost.org](http://www.boost.org) 获得更多关于Mpl的信息。另外,也可以看一下David Abrahams 和 Aleksey Gurtovoy 的书, C++ Template Metaprogramming! ``` #include <iostream> #include "boost/utility/enable_if.hpp" #include "boost/type_traits.hpp" #include "boost/mpl/has_xxx.hpp" BOOST_MPL_HAS_XXX_TRAIT_DEF(type) void some_func(int i) { std::cout << "void some_func(" << i << ")\n"; } template <typename T> void some_func(T t, typename boost::enable_if<has_type<T> >::type* p=0) { typename T::type variable_of_nested_type; std::cout << "template <typename T> void some_func(T t)\n"; } ``` 这真的很酷!我们现在可以对没有嵌套类型`type`的`T`禁用`some_func`的模板版本了,而且我们清晰地表达了这个函数的要求。这里的窍门在于使用了Boost.Mpl的一个非常漂亮的特性,它可以测试任意类型`T`是否内嵌有某个指定类型。通过使用宏 `BOOST_MPL_HAS_XXX_TRAIT_DEF(type)`, 我们定义了一个名为`has_type`的新的trait,我们可以在函数`some_func`中使用它作为`enable_if`的谓词。如果谓词为`True`, 这个函数就是重载决议集合中的一员;如果谓词为 `false`, 这个函数就将被排除。 也可以包装返回类型,而不用增加一个额外的(缺省)参数。我们最后一个也是最好的一个 `some_func`, 在它的返回类型中使用 `enable_if` ,如下: ``` template <typename T> typename boost::enable_if<has_type<T>,void>::type some_func(T t) { typename T::type variable_of_nested_type; std::cout << "template <typename T> void some_func(T t)\n"; } ``` 如果你需要返回你想启用或禁用的类型,那么在返回类型中使用 `enable_if` 和 `disable_if` 会比增加一个缺省参数更合适。另外,有可能有的人真的为缺省参数指定一个值,那样就会破坏这段代码。有时,类模板的特化也需要被允许或被禁止,这时也可以使用 `enable_if`/`disable_if` 。不同的是,对于类模板,我们需要对主模板进行一些特别的处理:增加一个模板参数。考虑一个带有返回一个`int`的成员函数`max`的类模板: ``` template <typename T> class some_class { public: int max() const { std::cout << "some_class::max() for the primary template\n"; return std::numeric_limits<int>::max(); } }; ``` 假设我们决定对于所有算术类型(整数类型及浮点数类型), 给出一个特化版本的定义,`max` 返回的是该算术类型可以表示的最大值。那么我们需要对模板类型`T`使用`std::numeric_limits`,而对其它类型我们还是使用主模板。要做到这样,我们必须给主模板加一个模板参数,该参数的缺省类型为 `void` (这意味着用户不需要显式地给出该参数)。结果主模板的定义如下: ``` template <typename T,typename Enable=void> class some_class { public: int max() const { std::cout << "some_class::max() for the primary template\n"; return std::numeric_limits<int>::max(); } }; ``` 现在我们已经为提供特化版本作好了准备,该特化版本为算术类型所启用。该特性可通过 Boost.Type_traits 库获得。以下是特化版本: ``` template <typename T> class some_class<T, typename boost::enable_if<boost::is_arithmetic<T> >::type> { public: T max() const { std::cout << "some_class::max() with an arithmetic type\n"; return std::numeric_limits<T>::max(); } }; ``` 该版本只有当实例化所用的类型为算术类型时才会启用,这时特性 `is_arithmetic` 为 `true`. 它可以正常工作是因为 `boost::enable_if&lt;false&gt;::type` 是 `void`, 会匹配到主模板。以下程序用不同的类型测试这个模板: ``` #include <iostream> #include <string> #include <limits> #include "boost/utility/enable_if.hpp" #include "boost/type_traits.hpp" // Definition of the template some_class omitted int main() { std::cout << "Max for std::string: " << some_class<std::string>().max() << '\n'; std::cout << "Max for void: " << some_class<void>().max() << '\n'; std::cout << "Max for short: " << some_class<short>().max() << '\n'; std::cout << "Max for int: " << some_class<int>().max() << '\n'; std::cout << "Max for long: " << some_class<long>().max() << '\n'; std::cout << "Max for double: " << some_class<double>().max() << '\n'; } ``` 我们预期前两个 `some_class` 会实例化主模板,剩下的将会实例化算术类型的特化版本。运行该程序可以看到的确如此。 ``` some_class::max() for the primary template Max for std::string: 2147483647 some_class::max() for the primary template Max for void: 2147483647 some_class::max() with an arithmetic type Max for short: 32767 some_class::max() with an arithmetic type Max for int: 2147483647 some_class::max() with an arithmetic type Max for long: 2147483647 some_class::max() with an arithmetic type Max for double: 1.79769e+308 ``` 一切正常!以前,要允许或禁止重载函数和模板特化需要一些编程的技巧,多数看到代码的人都不能完全明白。通过使用 `enable_if` 和 `disable_if`, 代码变得更容易写也更容易读了,并且可以从声明中自动获得正确的类型要求。在前面的例子中,我们使用了模板 `enable_if`, 它要求其中的条件要有一个名为`value`的嵌套定义。对于多数可用于元编程的类型而言这都是成立的,但对于整型常量表达式则不然。如果没有名为`value`的嵌套类型,就要使用 `enable_if_c` 来代替,它接受一个整型常量表达式。使用 `is_arithmetic` 并直接取出它的值,我们可以这样重写`some_class`的启用条件: ``` template <typename T> class some_class<T, typename boost::enable_if_c< boost::is_arithmetic<T>::value>::type> { public: T max() const { std::cout << "some_class::max() with an arithmetic type\n"; return std::numeric_limits<T>::max(); } }; ``` `enable_if` 和 `enable_if_c`原则上并没有不同。它们的区别仅在于是否要求有嵌套类型value。 ### 总结 被称为SFINAE的C++语言特性是很重要的。没有它,很多新的代码会破坏已有的代码,并且某些类型的函数重载(以及模板特化)将会无法实现。直接使用SFINAE来控制特定的函数或类型,使之被允许或被禁止用于重载决议,会很复杂。这样也会产生难以阅读的代码。使用 `boost::enable_if` 是更好的办法,它可以规定重载仅对某些特定类型有效。如果相同的参数用于 `disable_if`, 则规定重载对于符合条件的类型无效。虽然使用SFINAE也可以实现,但该库可以更好地表达相关意图。本章忽略了`enable_if` 和 `disable_if`的lazy版本(名为 `lazy_enable_if` 和 `lazy_disable_if`), 不过我在这里简单地提及一下。lazy版本被用于避免实例化类型可能无效的情形(取决于条件的取值). 以下情形时使用 `enable_if` : * 你需要在把一个符合某些条件的函数加入到或排除出重载决议集合中。 * 你需要根据某个条件将一个类模板的特化版本加入到或排除出特化集合中。