Tag Archives: type_traits

Проверка возможности записи через итератор вывода

Периодически возникает желание иметь статическую проверку согласованности типа значения итератора вывода и типа записываемого по итератору значения. К сожалению, в том случае, когда в качестве итератора передаётся настоящий итератор вывода, может оказаться, что iterator_traits<OutIt>::value_type есть void. Таковы, в частности, стандартные итераторы вывода, например, back_insert_iterator и ostream_iterator. Причина такого поведения заключается в том, что данные итераторы допускают только присваивание через разыменование и инкремент (который может ничего не делать):

*out = val;

Тип значения, возвращаемого оператором * обычно не является ссылкой на тип записываемого значения (это может быть прокси-объект, в качестве которого может выступать и сам итератор). Подстановка void в качестве value_type приводит к скорым ошибкам компиляции при попытке некорректно использовать этот тип.

Если требуется только проверить доступность такой операции записи для заданного типа значения, то это следует сделать напрямую — проверить возможность указанного выше присваивания:

#include <type_traits>

/// Check if a value type can be written via an iterator.
template <class Val, class It>
using Is_writable_to = typename std::is_assignable<
    decltype(*std::declval<It>()), Val>::type;
     
/// Check if a value type can be written via an iterator.
template <class Val, class It>
constexpr bool is_writable_to 
    = Is_writable_to<Val, It>::value;

Теперь следующий код выводит «110» (double можно привести к int, а void* — нельзя):

#include <iostream>
#include <iterator>
#include <vector>

int main()
{
  using namespace std;
  vector<int> a;
  auto bi = back_inserter(a);
   
  cout << is_writable_to<int, decltype(bi)>;
  cout << is_writable_to<double, decltype(bi)>;
  cout << is_writable_to<void*, decltype(bi)>;
  return 0;
}
Реклама

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;
}