Comments 31
+ для перфекционистов код несколько проще.
https://habrahabr.ru/post/159389/
https://habrahabr.ru/post/166589/
В принципе, я мог бы и про свою реализацию написать: https://github.com/SBKarr/stappler/blob/v2/common/apr/SPAprFunction.h, фича в использовании пулов памяти по образцу Apache Portable Runtime вместо new/delete, плюс отдельный скоростной тип для заворачивания лямбд (и только их) в колбеки, ибо архитектурно это разные подходы. (пока смотрел код, заметил пропущеyный std::forward...). Но, имхо, здесь маловато новаторства, потому статьи оно недостойно.
Просто хочу заметить, что "если не сильно менять и причёсывать" код из статьи, то, как раз таки, std::forward()
там нигде не нужно, просто потому что форвардинг ссылки нигде и не используются (а должны! К сожалению, и std::move()
упущен… ладно).
По поводу вашей реализации, извините, я не сильно всматривался, вам не хватило аллокатора который есть в интерфейсе std::function?
Плюс, по поводу std::forward()
, в этом месте — кхм, навскидку не понял, почему там std::forward()
, а не std::move()
? Судя по этому месту, я дико извиняюсь, — вы неправльно используете std::forward()
?
Допустим, у нас есть:
#include <cstdio>
#include <utility>
struct UserType
{
};
// (1)
void Concrete(UserType&&)
{
std::puts("Concrete(UserType&&)");
}
// (2)
void Concrete(UserType&)
{
std::puts("Concrete(UserType&)");
}
template<typename T>
void DoForward(T&& parameter)
{
Concrete(std::forward<T>(parameter));
}
int main()
{
UserType value;
// 1: аргумент (@value) у нас T& (UserType&, lvalue),
// а параметр (@parameter) - T&& - происходит наложение
// & + && -> получаем UserType&.
// Вызывается (2)я версия Concrete()
DoForward(value);
// 2: Передаём временный обьект -
// аргумент (@value) у нас T&& (UserType&&, грубо говоря, rvalue),
// а параметр (@parameter), всё тот же, T&& - происходит наложение
// && + && -> получаем UserType&&.
// Вызывается (1)я версия Concrete()
DoForward(UserType{});
}
parameter в DoForward()
, поскольку это шаблонная функция, попадает в (не знаю как перевести) "deduced context" — и всё что в main
-е я написал, это, как раз таки, "вывод типа" параметра, который учитывает "шаблонный" тип параметра и тип аргумента функции. Из-за того, что это шаблон, как видно, мы можем получить либо rvalue либо lvalue. Всё просто: "deduced context" — юзаем forward()
.
Как работает forward()
: для первого вызова в main
-е, как я уже написал, тип parameter вывелся в UserType&
, т.е., T
— это UserType&
. forward()
принимает аргументом (всегда!) — именованную переменную — т.е., это всегда lvalue. Получаем, что forward()
имеет на вход шаблонный аргумент типа UserType&
и тип параметра, так же, UserType&
— всё это означает, что переданный на вход аргумент — это lvalue! forward()
ничего не делает.
Аналогично, для второго вызова в main
-е, тип parameter вывелся в UserType&&
, т.е., T
— это UserType&&
. Получаем, что forward()
имеет на вход шаблонный аргумент типа UserType&&
и параметр UserType&
— переданный на вход аргумент — это rvalue! forward()
делает move()
.
Во всех остальных случаях — у нас нет "deduced context-а" и всё, каким мы его видим, таким и есть — т.е. T&&
— это rvalue — нужно делать move()
. T&
— это ссылка — делаем что хотим, а T
— хм, переменная, которая, больше не используется, поэтому можно сделать move()
.
Т.е., в случае:
template<typename T>
struct NonDeducedContext
{
static void call(T parameter)
{
DoForward(std::move(parameter));
}
}
для параметра функции call()
нет вывода типа, потому что он уже выведен для класса в целом (мы его указываем при инстанциировании шаблона: NonDeducedContext<UserType>
). Т.е., parameter — это value type — это копия аргумента функции call()
. Ниже, по коду, он нигде не используется, поэтому я спокойно его муваю, тем самым говоря, что я его больше не использую и "делайте со мной что хотите".
template <typename… Args>
struct F {
void f(Args… args) {
…
}
}
В таком случае, parameter pack должен состоять из <const string &, vector &&>. Когда мы раскрываем такой пакет, по виду moveNamedVector(args...), каждый элемент попадает в deduced context, и при этом имеет имя (пусть виртуальное и недоступное пользователю), а значит, каждый элемент без использования forward будет передан как lvalue. В итоге компилятор будет искать сигнатуру void moveNamedVector(const string &, const vector &), которая не соответствует желаемой. С использованием move(args)… компилятор попробует получить rvalue из всего переданного, и будет искать void (string &&, vector &&), опять промах. Остаётся использовать std::forward. Кстати, gcc и clang в их реализации function со мной согласны.
P.S. Оказывается GCC требует признать аллокаторы для std::function устаревшими и не поддерживает их совсем. А в реализации MSVC аллокаторы не проверяются на совместимость при move assignment, что, вапще говоря, серьёзное упущение. Хорошо, что я туда не полез из других соображений, а то долго бы ловил получившийся гейзенбаг.
Не, долго писать, но вкратце:
С использованием move(args)… компилятор попробует получить rvalue из всего переданного, и будет искать void (string &&, vector &&), опять промах. Остаётся использовать std::forward.
Если вы будете использовать std::forward()
в этом контексте, то это аналогично std::move()
для всех параметров. Т.е., с вашим примером, будет std::move()
как для 1го аргумента, так и для второго, т.е., если была бы ещё перегрузка moveNamedVector(string&&, vector&&)
, то вызвалась бы именно она, независимо от того использовали ли бы вы std::move()
(более идиоматично) либо std::forward()
(нестандартное использование).
По поводу std::function
, да, использование с аллокаторами задепрекейтили в C++17: Deprecating Allocator Support in std::function, потому что:
there are technical issues with storing an allocator in a type-erased context and then recovering that allocator later for any allocations needed during copy assignment.
Забавно
Во-первых, на таких простых примерах запросто может происходить девиртуализация, так что не факт, что эксперимент корректен.
Во-вторых, статическая переменная инициализируется один раз, поэтому чудо-объект Wrap
одноразовый.
Не занимайтесь ерундой и либо используйте шаблоны (для гарантированного встраивания), либо, если нет возможности, берите std::function
. В гцц она гарантированно не делает виртуальных вызовов, а для маленьких функциональных объектов даже не выделяет динамическую память.
Код по ссылке не подвержен девиртуализации. Собственно ее для тестом можно исключить выбирая метод по пользовательскому вводу.
Про std::function опять-же для других целей все замечательно. Для конкретно решавшейся задачи — это сильное усложнение и кода и механизма.
что вроде бы и логично
Нет, не логично. Могут быть объекты одного типа, но с разным состоянием.
Код по ссылке не подвержен девиртуализации. Собственно ее для тестом можно исключить выбирая метод по пользовательскому вводу
Пользовательский ввод сам по себе не гарантирует отключение девиртуализации. Нужно более серьёзное обоснование.
Про std::function опять-же для других целей все замечательно. Для конкретно решавшейся задачи — это сильное усложнение и кода и механизма.
Хотелось бы увидеть ассемблерный код ваших обёрток в сравнении с ассемблерным кодом std::function
на более-менее реальной программе.
Пока что весь ассемблерный код был показан только на вырванных из контекста участках кода. На которых компилятор, естественно, всё хорошо соптимизировал.
А лучше всего не ограничиваться ассемблером, а ещё и замерить реальное время работы.
Ситуацию с множественным вызовом Wrap я действительно не рассмотрел. Как только смогу — докину апдейт.
А вот про девиртуализацию можно поспорить. Зависимость от пользовательского ввода или других рантайм данных (скажем чтение из файла или банально результат random) не позволяет компилятору определить что именно он должен девиртуализировать. По крайней мере я не знаком и не находил подобных возможностей. Если можете — пожалуйста опишите конкретнее или дайте пример.
Может я не прав, но мне кажется, что у современных процессоров Интел время исполнения очереди команд такое не очевидное…
Раньше да, как-то можно было прикинуть в уме, хотя бы примерно одна команда один такт, но это правило уже давно не работает.
Далее, если есть условные или косвенные переходы — то можно ожидать, что такой код будет работать медленнее по сравнению со случаем, когда условных и косвенных переходов нет.
Также могут сильно тормозить обращения к памяти. Когда их меньше — то скорее всего будет работать быстрее.
Ну и «тяжелые» команды — вычисления с плавающей запятой, всякие там синусы, деление — все это тоже долго исполняется по сравнению с mov/add.
Обращения к памяти кажется не тормозят, видимо все в кеш попадало.
Переходы так же быстры и на них потерь не много — видимо хорошо работает предсказание переходов.
Я думаю, что ваш результат — это не повод отказываться от ассемблера во всех случаях, так же как и мои положительные результаты — не повод всегда ломиться в ассемблер. Тут нужен опыт и интуиция для принятия правильных решений. И добывается такой опыт практикой.
2) При желании можно открыть справочник по процессорам (собственно что и делалось), и оценить расходы на выполнение каждой инструкции (хотя-бы приблизительно для целевого железа).
внутри неё определёна статическая переменная W — значит, для всех вызовов с одинаковым аргументом темплейта (Func) будет работать с одной и той же переменной W (проинициализирована она будет только при первом вызове для данного Func).
Т.е. вызов Wrap для нескольких функторов одного типа вернёт один и тот же результат.
Я где-то ошибся? (проверять сейчас некогда)
Про default (если снова таки я правильно понял вопрос) то всё достаточно просто -для корректного удаления виртуальных объектов нужен виртуальный деструктор, при этом его тело в данном случае тривиально (потому default). На самом деле в данном примере удаление происходит не через работу с указателями, потому деструктор можно не описывать вообще. Но в дань правильному коду и во избежании потенциальных ошибок я решил его описать.
Встраивание функциональных объектов, функций и лямбд через шаблоны и унификация при помощи virtual на C++