Это небольшой отчет о том, как я решил написать метафункцию is_function для С++98/03, таким образом, чтобы не нужно было создавать множество специализаций для разного количества аргументов.
Зачем, спросите вы, в 2016 году вообще подобным заниматься? Я отвечу. Это challenge. Кроме всего прочего, эта, сперва чисто теоретическая работа из разряда «возможно или нет», вскрыла некоторые проблемы в современных компиляторах. Всех, кому не чуждо подобное настроение, приглашаю взглянуть.
Постановка
Немного теории
Реализация №0
Реализация №1
Реализация №2
Вместо заключения
Постановка
Метафункция is_function<T>
возвращает true
если тип T
является типом «функция», и false
, если нет. Итак, нам требуется написать метафункцию без использования многочисленных специализаций для разного количества типов аргументов у исследуемого типа функции. На пример реализации со специализациями вы можете посмотреть в boost. На данный момент там 25 аргументов максимум и занимает все это достаточно много места.
Зачем делать специализации? В С++11 появилось замечательное средство — variadic templates. Появилось оно потому что назрело, можно сказать, наболело. Это средство позволяет обрабатывать вот такие последовательности аргументов шаблона
some_template<A1, A2, A3 /*, etc. */>
как единый «пакет» параметров, parameter pack. Parameter pack помогает делать обобщенные специализации, перегрузки и подстановки в случаях, когда количество аргументов неизвестно. Именно с помощью этого средства в С++11 реализуется is_function. В С++98/03 такого средства не было. Это значит, что в общем случае, если нам необходимо было обеспечить разное количество аргументов в зависимости от ситуации, приходилось делать перегрузки и специализации «на все случаи жизни». Если вы посмотрите на реализации таких библиотек как variant или mpl в boost, то убедитесь в изобилии подобного кода (иногда его генерируют препроцессором). Применительно к нашей задаче, если нам требовалось определить, является ли тип T
функцией R(A1, A2)
, то самым простым и очевидным решением было создать соответствующую специализацию:
template <typename F>
struct is_function { /* .... */ };
template <typename R, typename A1, typename A2>
struct is_function<R(A1, A2)> { /* .... */ };
Зачастую, сделать по-другому было просто невозможно. Не хочу чтобы вы думали, будто бы я недоволен реализацией в boost – их решение наиболее переносимо, поэтому наиболее правильно, особенно в контексте такой библиотеки. Но мне было интересно в поставленной задаче обойтись без этого.
В общем, я один из тех, кому не повезло (хотя это как посмотреть) по долгу службы все еще работать с C++03. Поэтому моя озабоченность совместимостью со старым кодом не должна вас удивлять. Кто-то может сказать: «да далось тебе это старье, на дворе 2016 год!». С этим можно согласиться, но кроме чисто субъективного ощущения соревнования получилось извлечь из этого и определенную пользу. Да и дух С++03, в силу обозначенных выше причин, все еще не успел выветриться. Поэтому так, just for fun.
Прежде, чем мы перейдем к описанию, хочу предупредить, что от читателя потребуется понимание что такое SFINAE и базовых принципов написания compile-time проверок, а также, я не стану точно переводить формулировки из стандарта, и предполагаю, что заинтересованный читатель при желании в состоянии справиться с этим самостоятельно.
Немного теории
Если классический подход со специализациями нас не устраивает, то как же тогда быть? Давайте попробуем подумать немного иначе. Какие есть свойства у типа функции, которых более нет у других? Обратимся к стандарту (С++03):
4.3/1
An lvalue of function type T can be converted to an rvalue of type “pointer to T.” The result is a pointer to the function.
Итак, функция может быть неявно преобразована в указатель на функцию. Такое же свойство есть у массивов: массив неявно преобразуется в указатель. Посмотрим что есть еще:
8.3.5/3
After determining the type of each parameter, any parameter of type “array of T” or “function returning T” is adjusted to be “pointer to T” or “pointer to function returning T,” respectively.
Это значит, что тип «функция», будучи указанным как параметр другой функции неявно приобретает свойства указателя. На основе этого, в параграфе 13.1, описано, что вот такая декларация
void foo(int ());
соответствует такой (т.е. это одно и то же):
void foo(int (*)());
Вот это мы уже можем использовать. Мы можем написать проверку на основе этого и определить функция перед нами или нет. Впрочем все не так просто, как кажется, но об этом позже. А пока давайте посмотрим что еще можно использовать, основываясь на типе «функция»:
8.3.4/1
In a declaration T D where D has the form
D1 [ constant-expressionopt]
and the type of the identifier in the declaration T D1 is “derived-declarator-type-list T,” then the type of theidentifier of D is an array type. T is called the array element type; this type shall not be a reference type, the (possibly cv-qualified) type void, a function type or an abstract class type.
Ага, это тоже интересно. Т.е. мы не можем получить массив из элементов типа «функция». Наравне с этим, мы не можем получить массив ссылок, массив void
и массив с элементами типа абстрактного класса. Если написать проверку, которая отсечет остальные варианты, то мы сможем точно определить функция перед нами, или нет.
Резюмируем. У нас есть две отличительные особенности функций. Тип «функция»
Эти свойства мы и будем использовать в реализации задуманного.
Реализация №0, предварительная
Я хочу начать с первой особенности типа «функция», которую мы обозначили в предыдущем разделе. Речь о 8.3.5/3. Кратко сформулировать можно так:
Если
void( F ) == void( F * )
, где F
– проверяемый тип
, то F
– функция.
Все это довольно просто звучит. И моя первая реализация была тоже достаточно простой. Простой, но неверной. По этой причине я не буду приводить ее полностью, но хочу отдельно рассказать об одном свойстве, которое я в ней использовал. Рассмотрим вот такой код.
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 * );
Забегая вперед, скажу, что этот код исходит из ошибочных предпосылок. Но компиляторы Clang (до версии 3.4 включительно), компиляторы GCC (до версии 4.9), компиляторы из состава VS (cl 19.x, может и более ранние) собирали его так, как я ожидал. Расскажу, каким образом это работает и как я планировал это использовать. Для начала напишем объявление функции, которое поможет нам в проверках:
template <typename X>
static char (& check_is_function( X ) )
[
is_same<void(*)( F ), X>::value + 1
];
Если тип, переданный в check_is_function, совпадает с void(*)( F )
, то функция возвращает ссылку на массив из двух char
, eсли не совпадает — из одного char
(тип возвращаемого значения мы потом сможем проанализировать с помощью sizeof
). Везде здесь принимаем, что F
– это тип, который мы исследуем на принадлежность к типу «функция». Теперь, если оформить это в простой шаблон,
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
};
};
мы сможем удостовериться, что на упомянутых выше компиляторах для выражения вида
is_function<int()>::value;
is_function<int>::value;
typedef void fcv() const;
is_function<fcv>::value;
мы получим результаты 1, 0 и 1 соответственно (полный код можно найти здесь, а запустить здесь). Да, это не полностью рабочее решение, здесь мы не отличаем указатели на функции от функций, есть проблема со ссылками, void
и т.д. Но это все просто обходится и акцентировать внимание я хотел бы не на этом. Если мы запустим этот же пример на компиляторах новее указанных (GCC >= 4.9, Clang >= 3.5, cl 19.x), то убедимся, что вывод изменился. Теперь мы получим результаты 1, 0, 0 соответственно. Происходит это потому, что тип функции с cv-qualifier-seq (это тот самый const
или volatile
в конце), который подставлен в тип другой функции (приобретая при этом свойства указателя) перестал быть верной подстановкой аргумента в варианте:
template <typename F>
static void (* gen( void (F *) ) )( F );
Почему? Потому что с внедрением нового стандарта, в котором ясно написано (последний черновик),
8.3.1/4
Forming a pointer to function type is ill-formed if the function type has cv-qualifiers or a ref-qualifier;
изменился и подход компиляторов к подобному коду. Добавление звездочки к такому типу — это неверная подстановка, поэтому код перестал работать. В С++03 не было столь же четкого правила (почитать об изменениях можно здесь). Что, разумеется, не означает, что там это должно было быть разрешено. Однако туманность формулировок стандарта оставила возможность пропустить этот момент, о чем и написано по приведенной ссылке:
It is not sufficiently clear from the existing wording that pointers and references to function types containing cv-qualifiers or a ref-qualifier are not permitted and thus would result in a deduction failure if created during template argument substitution.
Поэтому многие современные компиляторы до сих пор этого не учитывают (например cl 18.x или icc 13.x и 14.x). Внимательный читатель наверное уже задался вопросом, что если явное добавление звездочки к типу «функции с cv-qualifier-seq» не разрешено, то неявное, при указании такого типа в качестве параметра, тоже не должно быть доступно. Да, вероятно, это так. Однако, на данный момент, нет ни одного компилятора, который бы это явно запрещал.
В стандарте С++03 есть такое:
8.3.5/4
A cv-qualifier-seq shall only be part of the function type for a nonstatic member function, the function type to which a pointer to member refers, or the top-level function type of a function typedef declaration.
, что говорит нам о достаточно узком контексте применения типов функций с cv-qualifier-seq. И наш случай вроде бы туда не вписывается.
Поэтому, будем держать в голове, что код, построенный на основе «мутации» типа функции в указатель не будет работать для всех случаев, но раз на данный момент он все-таки работает, я покажу полное решение. Думаю, это послужит пищей для размышлений о несовершенности мира.
Реализация №1 (рабочая)
Эта реализация была доработана с учетом ограничений, описанных в предыдущем разделе. Скорее всего (я не уверен на 100%, но на это многое указывает) эта реализация не полностью соответствует стандарту, и то, что она все-таки работает должно послужить поводом отправки багрепортов в поддержку как минимум трех современных компиляторов.
Основная проблема предыдущей реализации в том, что в случае функции с cv-qualifier-seq перестала работать подстановка с указателем. К счастью, в этой проблеме спрятан ключ к ее решению. Мы можем написать SFINAE-проверку, которая определит, возможно ли подставить к переданному типу указатель. Таким образом мы отсечем варианты, когда это невозможно. Проверка выглядит так:
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
};
};
Если подстановка P*
неверна, то выбирается перегрузка с эллипсисом. В зависимости от выбранной перегрузки sizeof
от возвращаемого значения вернет либо 1, либо 2. Вычитанием единицы мы добьемся значения value
0 или 1, где 1 получается в случае, если к типу возможно подставить указатель, и 0, если невозможно (далее я буду пользоваться этим приемом таким же образом). Теперь у нас есть возможность сформировать тип указателя на функцию, основываясь на этой проверке. Мы можем поступить разными способами — на основе перегрузки или специализации. Я покажу способ на основе перегрузки, т.к. он более переносим.
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();
Итак, мы сформировали тип type
– указатель на функцию, параметром которой является другой тип. Это тот тип, который мы исследуем на принадлежность к типу «функция». Итак, если
declfunc<F>() == void(*)( F )
то наш тип F
– функция. Удаление ссылки (remove_reference
) нужно обязательно, в этом случае мы автоматически получим неравенство в случае, если
F = R(&)(Args)
, или F = T &
т.к. после всех подстановок сравниваться будут следующие типы:
void(*)( R(*)(Args) )
и void(*)( R(&)(Args) )
или
void(*)( T )
и void(*)( T & )
соответственно. Эти типы, очевидно, не совпадают, что нам и требуется. В случае же, если тип F
– функция вида R(Args)
, то сравниваться будут
void(*)( R(*)(Args) )
и void(*)( R(Args) )
эти типы равны, исходя из вышеприведенных положений стандарта (8.3.5/3). В случае, если F
– функция вида R(Args) const
, то сравниваться будут
void(*)( R(Args) const )
и void(*)( R(Args) const )
эти типы тоже равны, что нам и требуется.
В случае, если F = T
(не функция), то сравниваться будут
void(*)( T * )
и void(*)( T )
эти типы не равны, что нам и требуется.
Теперь нам нужно собственно сравнить типы. Есть одно но, мы не можем здесь использовать обычную проверку на основе is_same
, т.к. аргументом нашей is_function
может быть и абстрактный тип, использование которого в этом контексте приведет к ошибке компиляции. Поэтому мы заменим is_same
на SFINAE-проверку следующего толка:
template <typename F>
static char (& is_function_check( void( F ) ) )[2];
template <typename F>
static char (& is_function_check( ... ) )[1];
Воспользуемся которой мы так:
value = sizeof( is_function_check<Tp>( declfunc<Tp>() ) ) - 1;
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
};
};
Для проверки того, что этот шаблон действительно работает, давайте напишем тестирующий макрос.
#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
И запустим на следующем
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);
}
Здесь можно посмотреть полный код примера и теста, а здесь запустить. Кстати, этот код работает также и для С++11. Тестировалось на GCC 4.4.x – 6.0, Clang 3.0 – 3.9, VS 2013 и VS 2105. Есть компиляторы, которые считают добавление указателя к F
c cv-qualifier-seq верной подстановкой (например icc 13.x). На этих компиляторах проверка работать не будет.
Реализация №2 (соответствующая стандарту)
Вспомним 8.3.4/1. Там говорилось, что функция — один из немногих типов, массив которых нельзя создать. Раз с предыдущим способом не все однозначно, может быть здесь нас ждет бóльшая удача? Давайте еще раз перечислим массивы каких типов мы не можем создать:
- ссылки
void
- абстрактные классы
- функции
Итак, нашу задачу теперь можно разбить на два этапа. Отсеять остальные типы с подобным поведением и написать SFINAE-проверку, определяющую можно ли создать массив заданного типа. Для начала давайте отсеем абстрактные классы. Хотя проще всего отсеять все классы сразу. Для этого нам понадобится метафункция:
template <typename Tp>
struct is_class;
Теперь нам нужно убрать из рассмотрения ссылки и void. Используем для этого следующее:
template <typename Tp>
struct is_lvalue_reference;
template <typename Tp>
struct is_void;
Вроде бы все, но чего-то не хватает. В самом деле, есть еще один тип, который не может быть элементом массива — это array of unknown bound (массивы неизвестного размера T[]
). Его нам тоже нужно отсеять. В принципе, можно не мучиться и отсеивать сразу все массивы.
template <typename Tp>
struct is_array;
Реализацию этих метафункций можно найти здесь, или взять, например, из boost.
Теперь пришло время сформировать основной шаблон:
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)
};
};
Проверку, может ли являться тип элементом массива, мы организуем через определение типа «указатель на массив», с элементами тестируемого типа в качестве параметра функции check_is_function
. Если подстановка неуспешна, значит тип F
является функцией.
В качестве теста возьмем предыдущий набор из реализации 1. Полный код можно посмотреть здесь, а запустить здесь. Эта реализация полностью соответствует стандарту и скорее всего будет работать на большинстве компиляторов. Этот код также работает и для С++11, нужно только дополнительно отсеивать rvalue-ссылки.
Вместо заключения
1) Я отправил три багрепорта насчет нелегального продвижения функции с cv-qualifier-seq до указателя:
В поддержку Clang.
В поддержку GCC.
В поддержку VS.
Как уже говорилось, я не уверен на 100%, но это единственный способ узнать мнение разработчиков по этому вопросу.
2) Полный код, который находится на моем гитхабе, немного отличается от приведенного в статье в лучшую сторону. Здесь были намеренно опущены некоторые детали и правила хорошего тона.
3) Спасибо за внимание :)
Автор: wander