📜 ⬆️ ⬇️

Another implementation of the is_function metafunction for C ++ 98/03

This is a small report on how I decided to write the is_function metafunction for C ++ 98/03 so that it would not be necessary to create a lot of specializations for a different number of arguments.
Why, you ask, in 2016 in general to do this kind of thing? I will answer. This is a challenge. Among other things, this, at first purely theoretical work from the category of "possible or not," revealed some of the problems in modern compilers. All who are not alien to such a mood, I invite you to look.

Staging
Some theory
Implementation number 0
Implementation number 1
Implementation number 2
Instead of conclusion

Staging


The is_function<T> returns true if type T is of type “function”, and false if not. So, we need to write a meta-function without using multiple specializations for a different number of argument types for the function type being examined. You can look at an example implementation with specializations in boost . At the moment there are 25 maximum arguments and it all takes up quite a lot of space.
Why make specialization? In C ++ 11, a wonderful tool appeared - variadic templates . It appeared because it was overdue, one might say, sore. This tool allows you to process such sequences of template arguments.
  some_template<A1, A2, A3 /*, etc. */> 
as a single “package” of parameters, parameter pack. Parameter pack helps to make generalized specializations, overloads and substitutions in cases where the number of arguments is unknown. It is through this tool in C ++ 11 that is_function is implemented. In C ++ 98/03 there was no such tool. This means that, in general, if we needed to provide a different number of arguments depending on the situation, we had to make overloads and specialization “for all occasions”. If you look at the implementations of such libraries as variant or mpl in boost, then make sure that such code is abundant (sometimes it is generated by the preprocessor). If we needed to determine whether type T function of R(A1, A2) , then the simplest and most obvious solution was to create an appropriate specialization:
  template <typename F> struct is_function { /* .... */ }; template <typename R, typename A1, typename A2> struct is_function<R(A1, A2)> { /* .... */ }; 
Often, it was simply impossible to do otherwise. I don’t want you to think that I’m dissatisfied with the implementation in boost - their solution is the most portable, therefore, the most correct, especially in the context of such a library. But I was interested in the task to do without it.
In general, I am one of those unlucky (although this is how to look) on duty to still work with C ++ 03. Therefore, my concern about compatibility with the old code should not surprise you. Someone can say: “yes this old stuff was given to you, it's 2016 in the yard!”. One can agree with this, but, apart from the purely subjective feeling of competition, it turned out to derive some benefit from this. And the spirit of C ++ 03, for the reasons outlined above, still has not had time to erode. Therefore, just for fun.
Before we move on to the description, I want to warn you that the reader will need an understanding of what SFINAE is and the basic principles of writing compile-time checks, and I will not translate the wording from the standard exactly. I assume that the interested reader, if desired, is able to cope with this independently.

Some theory


If the classical approach with specializations does not suit us, then how to be? Let's try to think a little differently. What are the properties of a function type that others no longer have? Referring to the standard (C ++ 03):
4.3 / 1
An example of the type of a pointer to a pointer to a function.
So, a function can be implicitly converted to a function pointer. Arrays have the same property: the array is implicitly converted to a pointer. Let's see what else there is:
8.3.5 / 3
If you have an “array of T” or “function returning”, you’re adjusted for to
This means that the “function” type, when specified as a parameter of another function, implicitly acquires the properties of a pointer. Based on this, in paragraph 13.1 , it is described that such a declaration
  void foo(int ()); 
corresponds to this (i.e., it is the same):
  void foo(int (*)()); 
This is what we can already use. We can write a check based on this and define a function in front of us or not. But everything is not as simple as it seems, but more on that later. In the meantime, let's see what else you can use, based on the type of "function":
8.3.4 / 1
In the declaration TD where D has the form
D1 [constant-expression opt ]
D1 is the derived-declarator-type-list-T, ”then it is the type of identifier of the identifier of the type. T is called the array element type; This is a void, a function type or an abstract class.
Yeah, that's interesting too. Those. we can not get an array of elements of type "function". Along with this, we cannot get an array of references, a void array, and an array with elements of the abstract class type. If we write a check that cuts out the remaining options, then we will be able to accurately determine the function in front of us or not.
We summarize. We have two distinctive features of the functions. Type "function"
We will use these properties in the implementation of our plans.
')

Implementation number 0, preliminary


I want to start with the first feature of the “function” type, which we outlined in the previous section. It's about 8.3.5 / 3 .
It can be summarized as follows: the type F being checked is a function if equality holds
void( F ) == void( F * ) .
It all sounds pretty simple. And my first implementation was also pretty simple. Simple but incorrect. For this reason, I will not give it in full, but I want to tell separately about one property that I used in it. Consider this code.
  template <typename F> static void (* declfunc() )( F ); template <typename F> static void (* gen( void (F *) ) )( F ); template <typename F> static void (* gen( void (F ) ) )( F * ); 
Looking ahead, I will say that this code comes from erroneous assumptions. But Clang compilers (up to version 3.4 inclusive), GCC compilers (up to version 4.9), compilers from VS (cl 19.x, maybe earlier ones) compiled it as I expected. I'll tell you how it works and how I planned to use it. First, let's write the function declaration that will help us with the checks:
  template <typename X> static char (& check_is_function( X ) ) [ is_same<void(*)( F ), X>::value + 1 ]; 
If the type passed to check_is_function coincides with void(*)( F ) , then the function returns a reference to an array of two char , if it does not match - from one char (the type of the return value we can then analyze using sizeof ). Everywhere here we assume that F is a type that we examine for belonging to the type “function”. Now, if you put this into a simple template,
  template <typename F> struct is_function { template <typename X> static char (& check_is_function( X ) ) [ is_same<void(*)( F ), X>::value + 1 ]; enum { value = sizeof( check_is_function( gen( declfunc<F>() ) ) ) - 1 }; }; 
we can make sure that the compilers mentioned above to express the form
  is_function<int()>::value; is_function<int>::value; typedef void fcv() const; is_function<fcv>::value; 
we will get the results of 1, 0 and 1, respectively (the full code can be found here , and run here ). Yes, this is not a fully working solution, here we do not distinguish function pointers from functions, there is a problem with references, void , etc. But it all just costs and emphasize, I would not like that. If we run the same example on the newer compilers (GCC> = 4.9, Clang> = 3.5, cl 19.x), then we will make sure that the output has changed. Now we get the results 1, 0, 0, respectively. This happens because the type of the function with cv-qualifier-seq (this is the same const or volatile at the end), which is substituted into the type of another function (acquiring the properties of a pointer), has ceased to be a valid argument substitution in the variant:
  template <typename F> static void (* gen( void (F *) ) )( F ); 
Why? Because with the introduction of a new standard in which it is clearly written (the last draft),
8.3.1 / 4
Forming a function type cv-qualifiers or a ref-qualifier;
the compiler's approach to this code has changed. Adding an asterisk to this type is a wrong substitution, so the code stopped working. In C ++ 03 there was no equally clear rule (you can read about the changes here ). Which, of course, does not mean that it should have been allowed there. However, the nebula of standard language left the opportunity to skip this point, which is written on the following link:
It is not clear that it has been established during the submission.
Therefore, many modern compilers still do not take this into account (for example, cl 18.x or icc 13.x and 14.x). The attentive reader probably already wondered that if the explicit addition of an asterisk to the type of “function with cv-qualifier-seq ” is not allowed, then an implicit one, if you specify this type as a parameter, should not be available either. Yes, it probably is. However, at the moment, there is not a single compiler that explicitly prohibits this.
In the standard C ++ 03 there is this:
8.3.5 / 4
It is a function of a function of determination of a function.
This tells us about a rather narrow context of using function types with cv-qualifier-seq . And our case does not seem to fit there.
Therefore, we’ll keep in mind that the code built on the basis of the “mutation” of the function type in the pointer will not work for all cases, but since at the moment it is still working, I will show the complete solution. I think this will serve as food for thought about the imperfection of the world.

Implementation number 1, working


This implementation has been refined to meet the limitations described in the previous section. Most likely (I'm not 100% sure, but this indicates a lot) this implementation does not fully comply with the standard, and the fact that it still works should be the reason for sending bug reports in support of at least three modern compilers.
The main problem with the previous implementation is that in the case of a function with cv-qualifier-seq, the substitution with the pointer stopped working. Fortunately, in this problem the key to its solution is hidden. We can write an SFINAE check that determines whether it is possible to substitute a pointer to the type passed in. This way we cut off options when this is not possible. The check looks like this:
  template <typename F> struct may_add_ptr { template <typename P> static char (& may_add_ptr_check(P *) )[2]; template <typename P> static char (& may_add_ptr_check(...) )[1]; enum { value = sizeof( may_add_ptr_check<F>(0) ) - 1 }; }; 
If the P* substitution is incorrect, then an overload with an ellipsis is selected. Depending on the selected sizeof overload from the return value, returns either 1 or 2. By subtracting the unit we will get the value value 0 or 1, where 1 is obtained if it is possible to substitute a pointer to the type, and 0 if not (then I will use this reception in the same way). Now we have the opportunity to form the type of function pointer based on this check. We can do it in different ways - based on overload or specialization. I will show the method based on overload, because he is more portable.
  template <typename F> static typename enable_if< may_add_ptr<F>::value == 1, void (*)(typename remove_reference<F>::type *) >::type declfunc(); template <typename F> static typename enable_if< may_add_ptr<F>::value == 0, void (*)(typename remove_reference<F>::type ) >::type declfunc(); 
So, we have formed the type type - a pointer to a function whose parameter is another type. This is the type that we are investigating for belonging to the type “function”. So if
declfunc<F>() == void(*)( F )
then our type F is a function. The removal of the link ( remove_reference ) is necessary, in this case we will automatically get the inequality in case
F = R(&)(Args) , or F = T &
because after all substitutions, the following types will be compared:
void(*)( R(*)(Args) ) and void(*)( R(&)(Args) )
or
void(*)( T ) and void(*)( T & )
respectively. These types obviously do not match, which is what we need.
In the case, if the type F is a function of the form R(Args) , then they will be compared
void(*)( R(*)(Args) ) and void(*)( R(Args) ) .
These types are equal, based on the above provisions of the standard ( 8.3.5 / 3 ).
If F is a function of the form R(Args) const , then they will be compared
void(*)( R(Args) const ) and void(*)( R(Args) const ) .
These types are also equal, which is what we need.
If F = T (not a function), then they will be compared
void(*)( T * ) and void(*)( T ) .
These types are not equal, which is what we need.
Now we need to actually compare the types. There is one thing, but we cannot use the usual is_same based is_same , since The argument of our is_function can also be an abstract type, the use of which in this context will result in a compilation error. Therefore, we will replace the is_same with an SFINAE check of the following sense:
  template <typename F> static char (& is_function_check( void( F ) ) )[2]; template <typename F> static char (& is_function_check( ... ) )[1]; 
We use it like this:
  value = sizeof( is_function_check<Tp>( declfunc<Tp>() ) ) - 1; 
The full code looks like this.
  template <typename Tp> struct is_function { private: template <typename F> struct may_add_ptr { template <typename X> static char (& may_add_ptr_check(X *) )[2]; template <typename X> static char (& may_add_ptr_check(...) )[1]; enum { value = sizeof( may_add_ptr_check<F>(0) ) - 1 }; }; template <typename F> static typename enable_if< may_add_ptr<F>::value == 1, void (*)(typename remove_reference<F>::type *) >::type declfunc(); template <typename F> static typename enable_if< may_add_ptr<F>::value == 0, void (*)(typename remove_reference<F>::type ) >::type declfunc(); template <typename F> static char (& is_function_check( void( F ) ) )[2]; template <typename F> static char (& is_function_check( ... ) )[1]; public: enum { value = sizeof( is_function_check<Tp>( declfunc<Tp>() ) ) - 1 }; }; 

To verify that this pattern really works, let's write a testing macro.
  #define TEST_IS_FUNCTION(Type, R) \ std::cout << ((::is_function<Type>::value == R) ? "[SUCCESS]" : "[FAILED]") \ << " Test is_function<" #Type "> (should be [" #R "]):" \ << std::boolalpha \ << (bool)::is_function<Type>::value << std::endl 
And run on the following
test suite.
  struct S { virtual void f() = 0; }; int main() { typedef void f1() const; typedef void f2() volatile; typedef void f3() const volatile; TEST_IS_FUNCTION(void(int), true); TEST_IS_FUNCTION(void(), true); TEST_IS_FUNCTION(f1, true); TEST_IS_FUNCTION(void(*)(int), false); TEST_IS_FUNCTION(void(&)(int), false); TEST_IS_FUNCTION(f2, true); TEST_IS_FUNCTION(f3, true); TEST_IS_FUNCTION(void(S::*)(), false); TEST_IS_FUNCTION(void(S::*)() const, false); TEST_IS_FUNCTION(S, false); TEST_IS_FUNCTION(int, false); TEST_IS_FUNCTION(int *, false); TEST_IS_FUNCTION(int [], false); TEST_IS_FUNCTION(int [2], false); TEST_IS_FUNCTION(int **, false); TEST_IS_FUNCTION(double, false); TEST_IS_FUNCTION(int *[], false); TEST_IS_FUNCTION(int &, false); TEST_IS_FUNCTION(int const &, false); TEST_IS_FUNCTION(void(...), true); TEST_IS_FUNCTION(int S::*, false); TEST_IS_FUNCTION(void, false); TEST_IS_FUNCTION(void const, false); } 

Here you can see the full code of the sample and test, and here run. By the way, this code also works for C ++ 11. It was tested on GCC 4.4.x - 6.0, Clang 3.0 - 3.9, VS 2013 and VS 2015. There are compilers that consider adding a pointer to F with cv-qualifier-seq to be a valid substitution (for example, icc 13.x). On these compilers, verification will not work.

Implementation â„–2, corresponding to the standard


Recall 8.3.4 / 1 . It said that a function is one of the few types whose array cannot be created. Since the previous method is not all clear, maybe there will be more luck here? Let us once again list the types of arrays we cannot create:
  1. links
  2. void
  3. abstract classes
  4. functions
So, our task can now be divided into two stages. Weed out other types with a similar behavior and write an SFINAE check that determines whether it is possible to create an array of the specified type. First, let's eliminate the abstract classes. Although the easiest way to weed out all classes at once. For this we need a meta-function:
  template <typename Tp> struct is_class; 
Now we need to remove from consideration references and void. Use the following two templates for this:
  template <typename Tp> struct is_lvalue_reference; template <typename Tp> struct is_void; 
It seems to be all, but something is missing. In fact, there is another type that cannot be an element of an array — it is an array of unknown bound (arrays of unknown size T[] ). We also need to weed out. In principle, you can not suffer and sift out all the arrays at once.
  template <typename Tp> struct is_array; 
Implementation of these metafunctions can be found here , or take, for example, from boost.
Now it's time to create the main template:
  template <typename Tp> struct is_function { private: template <typename F> static char (& check_is_function( ... ) )[2]; template <typename F> static char (& check_is_function( F (*)[1] ) )[1]; public: enum { value = !is_class<Tp>::value && !is_void<Tp>::value && !is_lvalue_reference<Tp>::value && !is_array<Tp>::value && (sizeof( check_is_function<Tp>(0) ) - 1) }; }; 
Checking whether a type can be an element of an array is organized through the definition of the type “pointer to an array”, with elements of the type being tested as a parameter of the function check_is_function . If the substitution is unsuccessful, then type F is a function.
As a test, we take the previous set of implementation 1 . The full code can be viewed here , and run here . This implementation is fully compliant with the standard and most likely will work on most compilers. This code also works for C ++ 11, you only need to additionally filter out rvalue references.

Instead of conclusion


1) I sent three bug reports about illegal promotion of the function from cv-qualifier-seq to the pointer:
In support of Clang .
In support of the GCC .
In support of VS.
As already mentioned, I'm not 100% sure, but this is the only way to get the opinion of developers on this issue.

2) The full code that is on my github is a little different from the one given in the article for the better. Here some details and good tone rules were intentionally omitted.

2.1) I was asked to describe in more detail the types of functions with cv-qialifier-seq.
It is quite clear that this type cannot be used to declare a free function, since cv-qialifier-seq refers to this (see 9.3.1 / 3).
So in what situations does it work? The standard allows this for the following cases:
8.3.5 / 7
A typedef of a function type declarant includes a cv-qualifier-seq shall be used
only to declare a function, to declare a function
pointer to member refers to a function typedef function.
[Example:
 typedef int FIC(int) const; FIC f; // ill-formed: does not declare a member function struct S { FIC f; // OK }; FIC S::*pm = &S::f; // OK 
—End example]
Those. You can use the typedef function type with cv-qualifier-seq :
  • declare member functions
  • form a pointer to a member function and
  • use it as the top level type in the typedef declaration of another type of function.
The latter case is rather vague (what is meant by another function?). For comparison, I will quote from the last draft:
8.3.5 / 6
A type qualifier-seq or a ref-qualifier (7.1.3, 14.1) shall appear only as:
(6.1) - the function type for a non-static member function,
(6.2) - this function refers to
(6.3) - the function typedef declaration or alias-declaration,
(6.4) - the type-id of the type-parameter (14.1), or
(6.5) - the type-id of a template-argument for a type-parameter (14.3.1).
[Example:
 typedef int FIC(int) const; FIC f; // ill-formed: does not declare a member function struct S { FIC f; // OK }; FIC S::*pm = &S::f; // OK 
- end example]
Agree, it is much more understandable (note 6.3, now it legalizes the use of cv-qualifier-seq in the typedef type declaration of the top-level function and only , no another function). The fact that such a type can now be used as a parameter of a function is the subject of bug reports that I made. In addition, in this case, these types will be promoted to the pointer, and this is also prohibited ( 8.3.1 / 4 ).
Compare changes here . It is also possible that someone will be interested in this material .

3) Thank you for your attention :)

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


All Articles