Подтвердить что ты не робот

Как работает реализация функции std:: is_function Эрика Ниблера?

На прошлой неделе Eric Niebler твиттер очень компактная реализация для std::is_function класс признаков:

#include <type_traits>

template<int I> struct priority_tag : priority_tag<I - 1> {};
template<> struct priority_tag<0> {};

// Function types here:
template<typename T>
char(&is_function_impl_(priority_tag<0>))[1];

// Array types here:
template<typename T, typename = decltype((*(T*)0)[0])>
char(&is_function_impl_(priority_tag<1>))[2];

// Anything that can be returned from a function here (including
// void and reference types):
template<typename T, typename = T(*)()>
char(&is_function_impl_(priority_tag<2>))[3];

// Classes and unions (including abstract types) here:
template<typename T, typename = int T::*>
char(&is_function_impl_(priority_tag<3>))[4];

template <typename T>
struct is_function
    : std::integral_constant<bool, sizeof(is_function_impl_<T>(priority_tag<3>{})) == 1>
{};

Но как это работает?

4b9b3361

Ответ 1

Общая идея

Вместо перечисления всех допустимых типов функций, таких как примерная реализация на cpprefereence.com, в этой реализации перечислены все типы, которые не являются функциями, а затем разрешаются только true, если ни один из них не сопоставляется.

Список нефункциональных типов состоит из (снизу вверх):

  • Классы и союзы (включая абстрактные типы)
  • Все, что может быть возвращено функцией (включая void и ссылочные типы)
  • Типы массивов

Тип, который не соответствует ни одному из этих не-функциональных типов, является типом функции. Обратите внимание, что std::is_function явно рассматривает вызываемые типы, такие как lambdas или классы, с оператором вызова функции как не являющиеся функциями.

is_function_impl_

Мы предоставляем одну перегрузку функции is_function_impl для каждого из возможных не-функциональных типов. Объявления функций могут быть немного сложны для синтаксического анализа, поэтому давайте разбить его на примере классов и объединений:

template<typename T, typename = int T::*>
char(&is_function_impl_(priority_tag<3>))[4];

Эта строка объявляет шаблон функции is_function_impl_, который принимает один аргумент типа priority_tag<3> и возвращает ссылку на массив из 4 char s. Как принято с древних дней C, синтаксис объявления становится ужасно запутанным наличием типов массивов.

Этот шаблон функции принимает два аргумента шаблона. Первый - это просто неограниченный T, а второй - указатель на элемент T типа int. Часть int здесь не имеет значения, т.е. это будет работать даже для T, у которых нет элементов типа int. Тем не менее, это то, что это приведет к синтаксической ошибке для T, которая не относится к классу или типу объединения. Для тех других типов попытка создания шаблона функции приведет к ошибке замены.

Подобные трюки используются для перегрузок priority_tag<2> и priority_tag<1>, которые используют свои аргументы второго шаблона для формирования выражений, которые компилируются для T как допустимые типы возвращаемых функций или типы массивов соответственно. Только перегрузка priority_tag<0> не имеет такого сдерживающего второго параметра шаблона и поэтому может быть создана с любым T.

В целом мы объявляем четыре разных перегрузки для is_function_impl_, которые отличаются их входным аргументом и возвращаемым типом. Каждый из них принимает в качестве аргумента другой тип priority_tag и возвращает ссылку на массив char с различным уникальным размером.

Отправка тегов в is_function

Теперь, при создании экземпляра is_function, он создает is_function_impl с помощью T. Обратите внимание, что поскольку мы предоставили четыре различные перегрузки для этой функции, здесь должно иметь место перегрузочное разрешение. И поскольку все эти перегрузки являются функциональными шаблонами, это означает, что SFINAE имеет шанс зайти.

Итак, для функций (и только функций) все перегрузки будут сбой, кроме самого общего с priority_tag<0>. Так почему же инстанцирование не всегда разрешает эту перегрузку, если она самая общая? Из-за входных аргументов наших перегруженных функций.

Обратите внимание, что priority_tag создается таким образом, что priority_tag<N+1> публично наследуется от priority_tag<N>. Теперь, поскольку is_function_impl вызывается здесь с priority_tag<3>, эта перегрузка является лучшим совпадением, чем другие для разрешения перегрузки, поэтому сначала будет проверяться. Только если это не удается из-за ошибки замены, проверяется следующее наилучшее совпадение, которое представляет собой перегрузку priority_tag<2>. Мы продолжаем таким образом, пока не найдем перегрузку, которая может быть создана, или мы достигнем priority_tag<0>, которая не ограничена и всегда будет работать. Поскольку все нефункциональные типы покрываются более высокими перегрузками prio, это может произойти только для типов функций.

Оценка результата

Теперь мы проверяем размер типа, возвращаемого вызовом is_function_impl_, для оценки результата. Помните, что каждая перегрузка возвращает ссылку на массив char разного размера. Поэтому мы можем использовать sizeof, чтобы проверить, какая перегрузка была выбрана, и только установить результат на true, если мы достигли перегрузки priority_tag<0>.

Известные ошибки

Johannes Schaub нашел ошибку в реализации. Массив неполного типа класса будет неправильно классифицирован как функция. Это связано с тем, что текущий механизм обнаружения типов массивов не работает с неполными типами.