В процессе эпичного крестосрача на 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
Комментариев нет:
Отправить комментарий