⬆️ ⬇️

SFINAE is just

TLDR : how to determine if a type has a method with a given name and signature, as well as to recognize other properties of types, without going crazy.

image


Hello colleagues.

I want to talk about SFINAE, an interesting and very useful (unfortunately *) mechanism of the C ++ language, which, however, may seem to the unprepared person to be very brain-breaking. In fact, the principle of its use is quite simple and clear, being formulated in the form of several clear provisions. This article is intended for readers who have a basic knowledge of patterns in C ++ and are familiar, at least briefly, with C ++ 11.

* Why unfortunately? Although the use of SFINAE is an interesting and beautiful technique that has grown into a widely used idiom of a language, it would be much better to have means explicitly describing work with types.



First, just in case, I will very briefly talk about metaprogramming in C ++. Metaprogramming is the operations performed during the compilation of a program. Just as ordinary functions allow you to get values, types and compile-time constants are obtained using meta-functions. One of the most popular uses of metaprogramming, although far from the only thing, is recognizing type properties. All this, of course, is used in the development of templates: it is sometimes useful to know, we are dealing with a complex user class with a non-trivial constructor or with a normal int , somewhere it is necessary to establish whether one type is inherited from another, or whether one can be converted type in another. We will consider the mechanism of applying SFINAE in a classic example: checking whether a function member exists in a class with given types of arguments and a return value. I will try in detail and in detail to go through all the stages of creating a verification metafunction and to trace where it comes from.



The abbreviation SFINAE stands for substitution failure is not an error and means the following: when defining overloads of a function, erroneous instantiations of templates do not cause a compilation error, but are discarded from the list of candidates for the most appropriate overload. Humanly, this means:



It turns out that we have the ability to manipulate the overloads of a function, hiding some of them, if necessary, by creating artificial errors. In this case, the hidden candidate must be a template, and the fact of an error must depend on the parameter of this template. Other types of errors can be detected by the compiler without substitution, barely seeing the definition of the template itself, and SFINAE will fail, and instead we will be hurt.

')

Consider a simple example:

 int difference(int val1, int val2) { return val1 - val2; } template<typename T> typename T::difference_type difference(const T& val1, const T& val2) { return val1 - val2; } 


The difference function works great for integer arguments. But with custom data types, subtleties begin. The result of the subtraction is not always of the same type as the operands. So, the difference of two dates is the time interval, which itself is not a date. If the custom type MyDate has a typedef MyInterval difference_type; definition inside it typedef MyInterval difference_type; and the subtraction MyInterval operator - (const MyDate& rhs) const; , the template overload is applicable to it. Calling the difference(date1, date2) will be able to “see” both the template overload, and the version accepting an int , while the template overload will be considered more suitable.

The MyString type, in which there is no difference_type , during substitution will cause an error: the function would return a non-existent type. Calling the difference with arguments of type MyString can only “see” the int version of the function. This single version will be sufficiently suitable only if the conversion operator to a number is defined in MyString . The construction of val1 - val2 requires a binary minus operator and can also generate a syntax error. It turns out that the template function difference checks the type of the argument for the simultaneous fulfillment of three conditions at once: the presence of a difference_type , the presence of a subtraction operator and the possibility of reducing the result of a subtraction to the type difference_type (conversion is implied by the return operator). But while for types that violate the first condition, this overload is not visible, violation of the second or third condition will cause a compilation error.



Let us try to figure out how to make a metafunction that tells us if there is a void foo(int) method in some type. Caring STL, especially starting with version C ++ 11, has already identified many useful type_traits for us, which are mainly located in type_traits and limits headers, but for some reason it is precisely what we boldly planned to do. Metafunction usually looks like a template structure without data, within which the result of an operation is defined: a type specified through typedef with the name type or a static constant value . This agreement adheres to the STL, and we don’t have any reasons to be original, therefore we will adhere to the established pattern.



You can immediately write the "skeleton" of our future metafunction:
 template<typename T> struct has_foo{}; 


It determines the presence of a method, from which it is immediately clear that the result should have a Boolean type:
 template<typename T> struct has_foo{ static constexpr bool value = true; //  ,   ,     "". }; 


And now we need to figure out how to make an overload, which determines the type properties we need, and how to get a boolean constant from it. The beauty is that we don’t need to give bodies to overloads: since all the work is done in compilation mode due to type manipulations, just ads will suffice.

Obviously, we want our metafunction to be applicable for any type. After all, about any type, you can tell if there is a desired method in it. This means that has_foo should not cause compilation errors, no matter what parameter we set up. And an error will occur if it suddenly turns out that it turns out that we need two overloads of one test function. One of them, the “detector”, should be syntactically correct only for types that contain the desired method. The other, the “substrate,” must be omnivorous, that is, be sufficiently suitable for any substituted types. At the same time, the “detector” should have a distinct advantage in “suitability” over the “substrate”. The least priority and, at the same time, the most omnivorous in determining overloads is ellipsis (a three-dot designating a variable number of arguments):
 template<typename T> struct has_foo{ void detect(...); //  ""  . static constexpr bool value = true; // -,   ! }; 


Now we need to declare a “detector”. It must be a template: it is not enough that it is already inside a template structure! Need a template inside the template [for a few seconds, enjoy the approving views from the characters of the film Inception ]. This does not apply to the “substrate”, since we will never throw it out. But for the “detector” we will use the magic word decltype , which determines the type of expression, and the expression itself is not calculated and is not translated into code. Let's substitute as an expression a call for that method, with arguments of the desired type. Then the decltype answer is the return type of the method. And if there is no method with the same name, or it accepts other types of arguments, then we will get the very controlled error that we wanted. Let the “detector” return the same as foo :
 template<typename T> struct has_foo{ void detect(...); template<typename U> decltype(U().foo(42)) detect(const U&); static constexpr bool value = true; //   ! }; 




If we pass a reference to const T& to detect , it turns out that U is the same type as T To check the conformity of the return type, we will later refine the detector or think of something else along the way.

But wait! We call the method on a newly constructed anonymous object, and it is constructed by default. And what will happen if we has_foo type in has_foo that does not have a default constructor? Of course, a compilation error. It would be more correct to declare any function that returns a value of the desired type. It will not be called anyway, and the desired effect will be achieved. STL took care of this: in the header of the utility there is a declval function:
 template<typename T> struct has_foo{ void detect(...); template<typename U> decltype(std::declval<U>().foo(42)) detect(const U&); static constexpr bool value = true; //  ! }; 




It remains only to learn to distinguish the "substrate" from the "detector". Here all the same decltype will help us. The “substrate” type of the return value is always void , and the “detector” - the type returned by the method, that is, in the case when the method meets our requirements ... the same void . Will not work. Let's change the type for int to “substrate”. Then the check is simple: if the call to detect on the object T is of type void , then the “detector” worked and the method fully complies with our requirements. If the type is different, then either the “substrate” has worked, or the method exists, accepts those very arguments, but returns something wrong. We check how carefree STL is, and right there we find the type checking is_same for is_same equality:
 template<typename T> struct has_foo{ private: //     . static int detect(...); //     . template<typename U> static decltype(std::declval<U>().foo(42)) detect(const U&); public: static constexpr bool value = std::is_same<void, decltype(detect(std::declval<T>()))>::value; //  , . }; 


Hooray, we have achieved the desired. As you can see, everything is really quite simple. We pay tribute to those programmers who managed to do this focus in the harsh conditions of the previous standard, much more verbose and clever because of the lack of such useful things as declval .



SFINAE is used so widely that even in the caring STL, the special meta-function enable_if included. Its parameters are a Boolean constant and a type ( void by default). If true passed, then the type is present in metafunction: the one that is passed in the second parameter. If false passed, then there is no type there, which creates that very controlled error. In the light of the considerations listed above in a neat list, it must be remembered that enable_if can “cross out” the function overload only if it is a template, and also take care that the list of uncrowded overloads never remains completely empty. You can use enable_if in specializations of the template class, but in this case it is no longer SFINAE, but something like static_assert .



In conclusion, I want to focus on the fact that the potential of using this mechanism is much broader than the verification of type properties. You can use it directly for its intended purpose, creating optimized overloads of functions and methods: with random access iterators, for example, you can afford more liberties than with consecutive iterators. And if you wish, you can come up with much more bizarre designs, especially if your last name is Alexandrescu . Proceeding from the basic principles set forth in this note, you can create a powerful, flexible and reliable code that can adapt itself on the fly to the features of the types used.

Source: https://habr.com/ru/post/205772/



All Articles