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.
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; }
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.
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.
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.
template<typename T> struct has_foo{};
template<typename T> struct has_foo{ static constexpr bool value = true; // , , "". };
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; // -, ! };
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; // ! };
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.
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; // ! };
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; // , . };
declval
.
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
.
Source: https://habr.com/ru/post/205772/