Monthly Archives: Январь 2015

C++11: SFINAE реализация is_callable

В стандарт C++11 из Boost попал набор шаблонов, позволяющих выполнять некоторые манипуляции с типами или проверять наличие поддержки типами тех или иных операций. Данный набор размещён в стандартном заголовочном файле type_traits. Почему-то в этот набор предопределённых метафункций не попала функция, позволяющая определить, возможно ли вызвать объект некоторого типа (указатель на функцию, ссылку на функцию или функтор) для заданного набора параметров. То есть, проверить поддержку им заданной сигнатуры. Впрочем, такую метафункцию (я назвал её «is_callable») в рамках C++11 реализовать довольно легко.

Основной элемент C++, позволяющий проверять доступность той или иной операции, называется SFINAE — «substitution failure is not an error» — ошибка при подстановке параметров шаблона в шаблоне функции не считается ошибкой компиляции, а просто исключает данный вариант из рассмотрения при вызове перегруженной функции. Классическим примером использования SFINAE является стандартный шаблон enable_if (также взятый из Boost), позволяющий «включать» или «выключать» определения функций в зависимости от выполнения условий времени компиляции.

С появлением ключевого слова decltype, позволяющего «извлечь» тип выражения, появился новый паттерн, задействующий SFINAE в духе enable_if:

template <class T>
decltype((void)(Expr(T)), RetExpr()) foo(T);

Данный вариант функции пропадёт из рассмотрения при вызове, если выражение Expr(T) нельзя скомпилировать для данного типа T. В противном случае, возвращаемый тип функции будет совпадать с типом значения выражения RetExpr(). Приведение к void позволяет гарантировать «нормальное» поведение оператора «запятая» (а то вдруг он перегружен).

Такую конструкцию обычно лучше оформлять с «хвостовым» определением возвращаемого типа. Заодно можно будет воспользоваться именами параметров функции в выражении внутри decltype.

template <class T>
auto foo(T t) ->
    decltype((void)(Expr(t)), RetExpr());

В общем-то, данный приём составляет основу определения метафункции is_callable, для представления которого я написал данный пост. Оно довольно простое.

#include <type_traits>
#include <utility>

template <class F>
struct Try_to_call
{
  template <class... Args>
  static std::false_type can_call(Args&&...);

  template <class... Args>
  static auto can_call(F& f, Args&&... args)
    -> decltype((void)f(std::forward<Args>(args)...),
                std::true_type{});
};

template <class F, class... Args>
using is_callable = decltype
  (
    Try_to_call<F>::can_call(*(F*)nullptr,
          std::forward<Args>(*(Args*)nullptr)...)
  );

Как правило, определяют функцию, дающую вариант «по умолчанию» и для того имеющую максимально обобщённый вид. Здесь это вариант can_call, возвращающий false_type, принимающий любой набор параметров. Вообще любой.

Также можно задействовать примитивные типы с неявным приведением. Например, «вариант по умолчанию» принимает дополнительный параметр типа unsigned, а «желаемый вариант» — параметр типа int. Тогда при передаче, например, числа 1, int — предпочтительнее при выборе перегруженного варианта, однако unsigned тоже сойдёт, так как определено неявное приведение типа int -> unsigned.

Теперь пример использования. При запуске должно вывести 1110. Проверено в MSVC2013 и gcc 4.9.2.

#include <iostream>

int main()
{
  using namespace std;
  auto sqr = [](double x) { return x * x; };
  cout << is_callable<decltype(sqr), int>::value
       << is_callable<int (*)(int), double>::value
       << is_callable<int (*)(int), int>::value
       << is_callable<int (*)(int), double, double>::value
       << endl;
  return 0;
}
Реклама