enable_if


Header: "boost/utility/enable_if.hpp"

Sometimes, we wish to control whether a certain function, or class template specialization, can take part in the set of available overloads/specializations for overload resolution. For example, consider an overloaded function where one version is an ordinary function taking an int argument, and the other is a templated version that requires that the argument of type T has a nested type called type. They might look like this:

 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"; } 

Now, imagine what happens when you call some_func somewhere in your code. If the type of the argument is int, the first version is called. Assuming that the type is something other than int, the second (templated) version is called.

This is fine, as long as that type has a nested type named type, but if it doesn't, this code does not compile. Is this really a problem? Well, consider what happens when another integral type is used, like short, or char, or 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); } 

When compiling this program, you will get something like the following output from the frustrated compiler:

 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 

There it is. The template version of some_func has been chosen as the best overload, but the code in that version is not valid for the type short. How could we have avoided this? Well, we would have liked to only enable the template version of some_func for types with a nested type named type, and to ignore it for those without it. We can do that. The easiest way, which is not always an option in real life, is to change the return type of the template version like so:

 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; } 

If you haven't yet studied SFINAE (substitution failure is not an error),[8] chances are that you have a perplexed look on your face right now. When compiling with this update, our example compiles cleanly. The short is promoted to int, and the first version is called. The reason for this surprising behavior is that the template version of some_func isn't included in the overload resolution set anymore. It's excluded because when the compiler sees that the return type of the function requires that type be a nested type of the template type T (lots of types here), it knows that short doesn't fit the bill, so it removes the function template from the overload resolution set. This is what Daveed Vandevorde and Nicolai Josuttis have taught us to refer to as SFINAE, and it means that rather than producing a compiler error, that function simply is not considered as a valid overload for the type in question. If the type has such a nested type, though, it will be part of the overload set.

[8] See [3] in the Bibliography.

 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()); } 

The output when running this program is as follows:

 void some_func(12) void some_func(12) template <typename T> void some_func(T t) 

This works, but it's not a pretty sight. In this scenario, we had the luxury of playing with a void return type, which we could use for other purposes. If that hadn't been the case, we could have added another argument to the function and given it a default value.

 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"; } 

This version also uses SFINAE to disqualify itself from use with invalid types. The problem with both of these solutions is that they're really ugly, we have made them a part of the public interface, and they only work in some scenarios. Boost offers a much cleaner solution, which is both syntactically nicer and offers a much wider range of functionality than our earlier ad hoc solutions.

Usage

To use enable_if and disable_if, include "boost/utility/enable_if.hpp". For the first example, we'll disable the second version of some_func if the type of the argument is integral. Type information such as whether a type is integral is available in another Boost library, Boost.Type_traits. The enable_if and disable_if templates both accept a predicate controlling whether to enable or disable the function, respectively.

 #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"; } 

Although this is similar to what we did before, it's expressing something that we couldn't have done as easily using our direct approach, and this also has the advantage of documenting important information about the function in its signature. When reading this, it is clear that the function requires that the type T is not an integral type. It would be even better if we could enable it (and document it accordingly) only for types with a nested type, type, and we can do that if we use another library, Boost.Mpl.[9] Check this out:

[9] Boost.Mpl is beyond the scope of this book. Visit http://www.boost.org for more information on Mpl. Also, get your hands on David Abrahams's and Aleksey Gurtovoy's book, 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"; } 

This is very cool indeed! We are now disabling the template version of some_func when there is no nested type, type, in T, and we explicitly document that this is a requirement for the function in its signature. The trick here is to use a very nifty feature of Boost.Mpl that can test whether a certain nested type (or typedef) exists in an arbitrary type T. Using the macro invocation, BOOST_MPL_HAS_XXX_TRAIT_DEF(type), we define a new trait called has_type, which we use in the function some_func as the predicate for enable_if. If the predicate yields TRue, the function is part of the overload set; if it yields false, it is excluded.

It's also possible to wrap the return type rather than add an extra (defaulted) argument. The equivalent of our latest and greatest some_func, but using enable_if in the return type, looks like this.

 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"; } 

If you need to return the type that you need to enable or disable on, using enable_if and disable_if on the return type makes more sense than adding a defaulted argument. Also, there is a chance that someone actually provides a value instead of that default argument, which breaks the code. Sometimes, class template specializations need to be enabled or disabled, and enable_if/disable_if work for those cases too. The difference is that for class templates, we must give the primary template some special treatmentan additional template parameter. Consider a class template with a member function max that returns an int:

 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();   } }; 

Suppose we decide that for all arithmetic types (integral types and floating point types), there should be a specialization available that returns max as the maximum value that the type can hold. We'll thus need std::numeric_limits for the template type T, and we want all other types to use the primary template. To make this work, we must add a template parameter to the primary template, which has a default type of void (this means that users don't have to deal explicitly with this type). This results in the following primary template:

 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();   } }; 

We've now paved the way for providing a more specialized version, to be enabled if the type is arithmetic. That trait is available via the Boost.Type_traits library. Here's the specialization:

 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();   } }; 

This version is only enabled when instantiated with a type that is arithmeticthat is, when the trait is_arithmetic yields true. This works because boost::enable_if<false>::type is void, matching the primary template specialization. The following program tests these templates with various types:

 #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'; } 

We'd expect the first two uses of some_class to instantiate the primary template, and the rest to instantiate the specialization for arithmetic types. Running the program shows that this is indeed the case.

 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 

That's all there is to it! Enabling and disabling overloaded functions and template specializations has heretofore required some tricky programming, which most who read the code would not fully understand. By using enable_if and disable_if, the solution becomes easier to code and read, and automatically captures the type requirements right in the declaration. In the preceding example, we use the template enable_if, which expects that the condition has a nested definition called value. This is true for most metaprogramming-enabled types, but it certainly isn't for integral constant expressions, for example. When there is no nested type called value, use enable_if_c instead, which expects an integral constant expression. Naively using the trait is_arithmetic and extracting its value directly, we could have written the enabling condition for some_class, like so:

 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();   } }; 

There is no fundamental difference between enable_if and enable_if_c. It's only the expectation of a nested type value that sets them apart.

Summary

The C++ language feature known as SFINAE is important. Without it, a lot of new code would break existing code, and some types of function overloads (and template specializations) would simply not be possible. To directly make use of SFINAE in a controlled way to enable or disable certain functions or types for overload resolution is complicated. It also makes for hard-to-read code. Using boost::enable_if is an elegant way of simultaneously stating that an overload is only eligible when certain conditions apply. The same argument holds for disable_if, which is used to state the oppositethat an overload is not applicable if the condition holds true. There is promise of even more practical uses of SFINAE, and this library also serves as a very nice introduction of the topic. This chapter has omitted the lazy versions of enable_if and disable_if (named lazy_enable_if and lazy_disable_if), but I'll give them a brief mention here. The lazy versions are used to avoid instantiating types that may not be available (depending on the value of the condition).

Use enable_if when:

  • You need to add or remove a function to the overload set depending on some condition

  • You need to add or remove a class template specialization from the set of specializations depending on some condition



    Beyond the C++ Standard Library(c) An Introduction to Boost
    Beyond the C++ Standard Library: An Introduction to Boost
    ISBN: 0321133544
    EAN: 2147483647
    Year: 2006
    Pages: 125

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net