пятница, 17 июня 2016 г.

[prog.c++14] Парочка интересных моментов с шаблонами в современном C++

В процессе эпичного крестосрача на LOR-е довелось заглянуть в парочку темных мест C++, о которых раньше даже и задумываться не приходилось. Чуток подробностей для тех, кому это интересно, находится под катом.

Первый момент связан с тем, что в C++ нельзя взять адрес шаблона функции. Т.е. если для обычной функции мы можем написать вот так:

int call_f(int (*f)(int), int v) { return (*f)(v); }

int my_f(int a) { return a*a; }

call_f(my_f, 10);

То вот с шаблоном так не получится:

template<typename R, typename A>
R call_tf(R (*f)(A), A v) { return (*f)(v); }

template<typename A>
A my_tf(A a) { return a*a; }

call_tf(my_tf, 10);

Что, собственно говоря, не удивительно. Т.к. реальная функция, адрес которой можно взять, появляется только в момент инстанцирования шаблона. Поэтому можно сделать вот так:

call_tf(my_tf<int>, 10);

Но явное инстанцирование -- это не то, что может хотеть разработчик. Например, если он пишет обобщенный алгоритм, в который шаблон функции должен быть передан параметром. Что делать в этом случае?

Обходной вариант -- завернуть нужный нам шаблон функции в объект-функтор:

template<typename F, typename A>
auto call_tf(F f, A v) { return f(v); }

template<typename A>
A my_tf(A a) { return a*a; }

struct wrap_my_tf {
   template<typename A>
   auto operator()(A && a) const {
      return my_tf(std::forward<A>(a));
   }
};

call_tf(wrap_my_tf(), 10);
call_tf(wrap_my_tf(), 10u);
call_tf(wrap_my_tf(), 10.0f);

Вот здесь с этим фокусом можно поиграться в online.

На этом приеме, с использованием макросов и variadic-template, можно строить и еще более хитрые конструкции: вот здесь пример. Где и как это может потребоваться на практике без понятия. Но практика -- это такая штука, что заранее не предскажешь, с чем придется столкнуться со временем ;)


Второй момент связан как раз с использованием variadic-template.

Я лично предпочитаю писать обертки вроде показанной выше call_tf так, чтобы не расписывать подробно параметры функтора. Т.е. предпочитаю писать:

template<typename F, typename... ARGS>
auto call(F f, ARGS &&... args) {...}

А не вот так:

template<typename R, typename... ARGS>
auto call(R (*f)(ARGS...), ARGS &&... args) {...}

Одна из причин в том, что список типов аргументов, которые передаются в call, может не совпадать с типом списков аргументов f. Т.е. f может ожидать std::string и long, тогда как в call будут переданы const char (&)[N] и int. Вот пример, чтобы было понятно, о чем речь.

Но вот чего я не знал, так это того, что при форвардинге аргументов внутри call() нужно, чтобы количество аргументов для call в точности совпадало с количеством аргументов для f. Даже если у f есть аргументы со значениями по-умолчанию, их все равно нужно указывать. Т.е. вот такой пример компилироваться не будет:

#include <utility>
#include <iostream>
#include <string>

std::string f(std::string prefix, unsigned long value) {
  prefix += std::to_string(value);
  return prefix;
}

std::string f2(std::string prefix, unsigned long value,
               const std::string & suffix = std::string()) {
  prefix += std::to_string(value);
  prefix += suffix;
  return prefix;
}

template<typename F, typename... ARGS>
auto call(F f, ARGS &&... args) {
  return f(std::forward<ARGS>(args)...);
}

int main() {
  auto r = call(f, "value="42);
  auto r2 = call(f2, "value="42);
  auto r3 = call(f2, "value="42"i");
}

В общем, C++ развивается и становится мощнее и удобнее. В C++03 показанные выше трюки было сделать гораздо сложнее (если вообще возможно). Может быть со временем возможности по работе с шаблонами окажутся еще мощнее и интереснее. Но вряд ли в C++17, скорее уже в последующих стандартах.

Кстати, интересно, что язык Rust умудряется разобраться в ситуации, когда шаблон функции нужно передать параметром в другой шаблон: https://is.gd/WISvfg

Комментариев нет: