Наши проекты:
Журнал · Discuz!ML · Wiki · DRKB · Помощь проекту |
||
ПРАВИЛА | FAQ | Помощь | Поиск | Участники | Календарь | Избранное | RSS |
[3.138.199.50] |
|
Сообщ.
#1
,
|
|
|
Известно, что для std::function обязательно требуется конструктор копирования.
Однако, довольно регулярно требуется во всякие callback'и и потоки передавать non-copyable-объекты, типа std::unique_ptr, std::unique_lock и т.д. В свою очередь, std::bind возвращает вполне себе нормальный объект (не знаю какой), которому спокойно можно делать move. Поэтому решил написать обёртку для этого объекта: template <typename> struct CBinder; template <size_t... Indexes> struct CBinder<std::index_sequence<Indexes...>> { template <typename TFunc, typename... TT> static auto Bind(TFunc &&func, TT&&... args) { typedef std::tuple < decltype(std::placeholders::_1), decltype(std::placeholders::_2), decltype(std::placeholders::_3), decltype(std::placeholders::_4), decltype(std::placeholders::_5), decltype(std::placeholders::_6), decltype(std::placeholders::_7), decltype(std::placeholders::_8), decltype(std::placeholders::_9), decltype(std::placeholders::_10) > TPlaceholders; return std::bind(std::forward<TFunc>(func), std::forward<TT>(args)..., typename std::tuple_element<Indexes, TPlaceholders>::type{}...); } }; template <typename TRes, typename... TArgs> class CFunction { public: typedef std::unique_ptr<CFunction> TPtr; template <typename TFunc, typename... TT> static TPtr Make(TFunc &&func, TT&&... args) { typedef decltype(std::make_index_sequence<sizeof...(TArgs)>()) TArgsIndexes; auto fn = CBinder<TArgsIndexes>::Bind(std::forward<TFunc>(func), std::forward<TT>(args)...); return std::make_unique<CFunctionImpl<decltype(fn), CFunction>>(std::move(fn)); } virtual ~CFunction() {;} template <typename... TT> TRes Call(TT&&... args) { return _Call(std::forward<TT>(args)...); } template <typename... TT> TRes operator()(TT&&... args) { return Call(std::forward<TT>(args)...); } protected: virtual TRes _Call(TArgs... args) = 0; template <typename TFunc, typename T> class CFunctionImpl : public T { public: CFunctionImpl(TFunc &&fn) : m_fn(std::forward<TFunc>(fn)) { } virtual TRes _Call(TArgs... args) override { return m_fn(args...); } protected: TFunc m_fn; }; }; Использование: static void TestFunc(const char *__file__, int __line__, size_t p1, const char *p2) { std::cout << p1 << std::endl; std::cout << p2 << std::endl; std::cout << __file__ << std::endl; std::cout << __line__ << std::endl; } ........................ typedef CFunction<void, size_t, const char *> TFunc; auto spFunc = TFunc::Make(TestFunc, __FILE__, __LINE__); spFunc->Call(111, "22222"); std::unique_ptr<std::string> sp(new std::string("!!!!!!!!!!")); auto spFunc2 = TFunc::Make([sp = std::move(sp)](size_t p1, const char *p2) { std::cout << p1 << std::endl; std::cout << p2 << std::endl; std::cout << *sp << std::endl; }); spFunc2->Call(111, "22222"); Но, что-то такая реализация мне не очень нравится, в основном из-за лишнего вызова виртуального метода. Подозреваю, что есть "более лучшее" решение, но не могу придумать ничего умного. |
Сообщ.
#2
,
|
|
|
Цитата Олег М @ Известно, что для std::function обязательно требуется конструктор копирования. Можно подробнее? На сколько я знаю std::function можно присваивать даже лямбды, которые даже в теории не имеют конструкторы копирования. Если что - мне просто интересно. Я работал с std::function, но мало наверное. И не припомню ограничения с конструктором копирования. |
Сообщ.
#3
,
|
|
|
Цитата KILLER @ Можно подробнее? На сколько я знаю std::function можно присваивать даже лямбды, которые даже в теории не имеют конструкторы копирования. Вот примеры, лямбдами и без. Выдают ошибку error: use of deleted function ...., на конструктор копирования. Если в них заменить unique_ptr на shared_ptr, то всё будет нормально static void TestFunc(std::unique_ptr<std::string> &sp, const std::string &s) { } typedef std::function<void(const std::string &)> TFunc; std::unique_ptr<std::string> sp(new std::string()) ; TFunc fn = std::bind(TestFunc, std::move(sp), std::placeholders::_1); fn("1111111111111111111111"); TFunc fn2 = std::bind([](std::unique_ptr<std::string> &sp, const std::string &s) { }, std::move(sp), std::placeholders::_1); fn2("1111111111111111111111"); TFunc fn3 = std::bind([sp = std::move(sp)](const std::string &s) { }, std::placeholders::_1); fn3("1111111111111111111111"); |
Сообщ.
#4
,
|
|
|
Там проблема именно при инициализации объекта std::function тем объектом, который возвращает std::bind
static void TestFunc(std::unique_ptr<std::string> &sp, const std::string &s) { std::cout << *sp << " | " << s << std::endl; } typedef std::function<void(const std::string &)> TFunc; typedef std::function<void(std::unique_ptr<std::string>&, const std::string &)> TFunc2; static void Test1() { std::unique_ptr<std::string> sp(new std::string("0")); auto lambda = [&sp] (const std::string &s) { TestFunc(sp, s); }; // std::function инициализируется лямбда-функцией // откомпилируется нормально TFunc func_obj1 = lambda; func_obj1("1"); // std::function инициализируется указателем на функцию // откомпилируется нормально TFunc2 func_obj2 = TestFunc; func_obj2(sp, "2"); // объект, возвращаемый std::bind // откомпилируется нормально auto bind_obj = std::bind(TestFunc, std::move(sp), std::placeholders::_1); bind_obj("3"); // а вот это, действительно, не компилируется TFunc fn = bind_obj; } Почему так происходит, надо разбираться. Только там в исходном коде std черт ногу сломит :-) Цитата лямбды, которые даже в теории не имеют конструкторы копирования В смысле? Лямбду же можно скопировать: auto lambda = [&sp] (const std::string &s) { TestFunc(sp, s); }; auto lambda2 = lambda; |
Сообщ.
#5
,
|
|
|
Цитата Serpentus @ Почему так происходит, надо разбираться. Только там в исходном коде std черт ногу сломит :-) Не обязательно копаться в исходном коде. Там вроде заявлено, что std::function должна быть CopyConstructable. Хотелось бы знать - для чего? |
Сообщ.
#6
,
|
|
|
Цитата Олег М @ Хотелось бы знать - для чего? Так она реализована :-) Вот конструктор, который используется в данном случае template<class _Fx, class _Inv_res = typename _Mybase::template _Result_of_invoking_t<_Fx&>, class = typename _Mybase::template _Enable_if_returnable_t<_Inv_res> > function(_Fx _Func) { // construct wrapper holding copy of _Func this->_Reset(_STD move(_Func)); } Комментарий четко дает понять, что с параметра, переданного при инициализации, чем бы он ни был, снимается и сохраняется копия. Тот объект, который возвращает std::bind, копирование не позволяет, отсюда результат. Речь идет про тот частный случай std::bind, в другом случае эта функция может возвращать копируемый объект. auto bind_obj = std::bind(TestFunc, std::move(sp), std::placeholders::_1); auto bind_obj2 = bind_obj; // Не скомпилируется static void foo(int i) {} static void Test() { auto bind_obj = std::bind(foo, 1); auto bind_obj2 = bind_obj; // OK } |
Сообщ.
#7
,
|
|
|
Цитата Serpentus @ Комментарий четко дает понять, что с параметра, переданного при инициализации, чем бы он ни был, снимается и сохраняется копия Что–то не вижу я там копирования, вижу move. Ну и по–любому не понимаю – для чего там может понадобится обязательное копирование? |
Сообщ.
#8
,
|
|
|
Цитата Олег М @ Очевидно, что функциональные объекты интуитивно должны отвечать привычной программисту семантике значений. Так же как std::shared_ptr<> реализует это для указателей. Биндить в них значения суть нормальное явление, иначе зачем вообще использовать объекты вместо просто функций, однако это и подразумевает CopyConstructable для биндов. Там вроде заявлено, что std::function должна быть CopyConstructable. Хотелось бы знать - для чего? Пытаться сделать из не CopyConstructable объекта, того же std::uniqur_ptr<>, например, CopyConstructable объект не есть хорошая идея. Я бы не хотел какого-нибудь damned hell в своём проекте. Это когда в ряде случаев объекты вдруг оказываются пусты, потому что перемещены вовнутрь неведомых дебрей, и теперь живут там... или уже нет, хрен разберёшь. Иметь две разных семантики для одной парадигмы функционального объекта будет провоцировать на мат в дальнейшем. Лучше просто отказаться от биндов не CopyConstructable значений. |
Сообщ.
#9
,
|
|
|
Цитата Qraizer @ Лучше просто отказаться от биндов не CopyConstructable значений. В смысле, отказаться от передачи unique_ptr в поток или в callback? Или рассчитывать, что будут делаться копии твоих списков, строк и т.д.? |
Сообщ.
#10
,
|
|
|
В случае с потоками, вроде разобрался. Там прикол в том, что в отличие от std::bind, функция вызывается только один раз. И желательно, чтоб сразу после вызова все параметры удалялись, да и сама функция тоже. Чего, кстати, std::thread вроде не делает.
Написал, специально для потоков, вот такую штуку, не уверен, правда, что правильно template <typename> struct CThreadProcInvoker; template <size_t... Indexes> struct CThreadProcInvoker<std::index_sequence<Indexes...>> { template <typename TFunc, typename TArgs, typename... TT> static void Invoke(TFunc func, TArgs args, TT&&... args2) { std::invoke(func, std::move(std::get<Indexes>(args))..., std::forward<TT>(args2)...); } }; template <typename TFunc, typename... TArgs> class CThreadProc { public: explicit CThreadProc(TFunc &&fn, TArgs&&... args) : m_fn(std::forward<TFunc>(fn)) , m_args(std::forward<TArgs>(args)...) { } CThreadProc(CThreadProc &&) = default; CThreadProc& operator =(CThreadProc &&) = default; CThreadProc(const CThreadProc &) = delete; CThreadProc& operator =(const CThreadProc &) = delete; template <typename... TT> requires std::is_move_constructible<TFunc>::value void operator()(TT&&... args) { CThreadProcInvoker<std::index_sequence_for<TArgs...>>::Invoke(std::move(m_fn), std::move(m_args), std::forward<TT>(args)...); } protected: TFunc m_fn; std::tuple<std::decay_t<TArgs>...> m_args; }; template <typename TFunc, typename... TArgs> auto MakeThreadProc(TFunc &&fn, TArgs&&... args) { return CThreadProc<TFunc, TArgs...>(std::forward<TFunc>(fn), std::forward<TArgs>(args)...); } |
Сообщ.
#11
,
|
|
|
Цитата Олег М @ Что–то не вижу я там копирования, вижу move. move - это для передачи параметра в _Reset, копирование, очевидно, происходит в ней. |
Сообщ.
#12
,
|
|
|
Цитата Serpentus @ move - это для передачи параметра в _Reset, копирование, очевидно, происходит в ней. Всё равно непонятно, для чего там нужно копирование. |
Сообщ.
#13
,
|
|
|
Олег М
Копирование нужно, чтобы сохранить нечто, которым мы инициализируем std::function, где-то там у себя в члене класса. Надо запомнить то, что мы потом вызывать будем. Не ссылку же на него сохранять. Я говорю, по исходникам лазить - умрешь, но можно предположить, что там что-то вроде такого (очень упрощенно): struct _CallableBase { virtual ~_CallableBase() = default; }; template<typename T> struct _Callable : public _CallableBase, public T { }; template<...> class function { ... template<typename TFunc> function(TFunc func) { // сохраняем что-то, что потом можно вызывать через operator () call_obj = new _Callable<TFunc>(func); // тут с func надо снять копию } ... private: _CallableBase* call_obj; }; |
Сообщ.
#14
,
|
|
|
Цитата Serpentus @ но можно предположить, что там что-то вроде такого (очень упрощенно): У меня разве не то же самое, только без копии? |
Сообщ.
#15
,
|
|
|
Олег М
Так вопрос же вроде был, зачем в std::funtion требование CopyConstructable, не? Или ты имеешь в виду, почему они не сделали что-то типа того, как у тебя? Это можно только гадать. Может просто не додумались. Или не посчитали нужным (или даже вредным, Qraizer объяснил почему). А еще у вызова через std::function нулевой оверхед, а у вызова через std::unique_ptr<CFunction> - нет (пусть и не большой). |
Сообщ.
#16
,
|
|
|
Цитата Serpentus @ Это можно только гадать. Может просто не додумались. Или не посчитали нужным (или даже вредным, Qraizer объяснил почему). Всегда должны быть причины. Очень сильно сомневаюсь, что они там до чего-то не додумались. Цитата Serpentus @ . А еще у вызова через std::function нулевой оверхед, а у вызова через std::unique_ptr<CFunction> - нет (пусть и не большой). Подозреваю, что std::function реализована точно также - виртуальный метод, динамическое создание класса и т.д. Т.е. оверхед абсолютно такой же. |
Сообщ.
#17
,
|
|
|
В общем, остановился вот на таком варианте
template <typename> struct CFunctionInvoker; template <size_t... Indexes> struct CFunctionInvoker<std::index_sequence<Indexes...>> { template <typename TFunc, typename TArgs, typename... TT> static auto Invoke(TFunc &&func, TArgs &&args, TT&&... args2) { return std::invoke(func, std::get<Indexes>(args)..., std::forward<TT>(args2)...); } template <typename TFunc, typename TArgs, typename... TT> static auto InvokeOnce(TFunc func, TArgs args, TT&&... args2) { return std::invoke(func, std::move(std::get<Indexes>(args))..., std::forward<TT>(args2)...); } template <typename TFunc, typename... TT> static auto Invoke(TFunc &&func, std::tuple<>, TT&&... args2) { return std::invoke(func, std::forward<TT>(args2)...); } template <typename TFunc, typename TArgs, typename... TT> static auto InvokeOnce(TFunc func, std::tuple<>, TT&&... args2) { return std::invoke(func, std::forward<TT>(args2)...); } }; template <typename TRes, typename... TArgs> class CFunction { public: typedef std::unique_ptr<CFunction> TPtr; template <typename TFunc, typename... TT> static TPtr Make(TFunc &&func, TT&&... args) { return std::make_unique<CFunctionImpl<CFunction, TFunc, TT...>>(std::forward<TFunc>(func), std::forward<TT>(args)...); } virtual ~CFunction() {;} template <typename... TT> TRes Call(TT&&... args) { return _Call(std::forward<TT>(args)...); } template <typename... TT> TRes operator()(TT&&... args) { return Call(std::forward<TT>(args)...); } protected: virtual TRes _Call(TArgs... args) = 0; template <typename TFunction, typename TFunc, typename... TT> class CFunctionImpl : public TFunction { public: CFunctionImpl(TFunc &&fn, TT&&... args) : m_fn(std::forward<TFunc>(fn)) , m_args(std::forward<TT>(args)...) { } virtual TRes _Call(TArgs... args) override { return CFunctionInvoker<std::index_sequence_for<TT...>>::Invoke(m_fn, m_args, args...); } protected: TFunc m_fn; std::tuple<std::decay_t<TT>...> m_args; }; }; Избавился от std::placeholders. На большее меня не хватает. |
Сообщ.
#18
,
|
|
|
Цитата Serpentus @ Я уверен, что правильно указал причину. Во всех официальных вариантах реализаций, что в крайне скудном функционале C++98, что в более мощном варианте, появившемся в бусте, функциональные объекты полностью отвечали семантике значений. Их можно было свободно и копировать, и присваивать, в общем делать всё то, что обычный программист привык делать с обычными переменными. Если б в C++11 решили, что это не нужно, программисты б сильно удивились. ...Это можно только гадать. Может просто не додумались. Или не посчитали нужным (или даже вредным, Qraizer объяснил почему). |
Сообщ.
#19
,
|
|
|
Цитата Qraizer @ Их можно было свободно и копировать, и присваивать, в общем делать всё то, что обычный программист привык делать с обычными переменными. Там не в копировании проблема, а отсутствии move |
Сообщ.
#20
,
|
|
|
Так я как раз об этом. Копирование и перемещение – это разные семантики. Я бы не хотел вдруг узнать что после a=b источник разрушен. Конечно, по дефолту так и не будет, ибо нужно хотя бы a=std::move(b) написать. Но дело не в этом. Из-за документированной семантики копирования куча библиотек уже использовали функциональные объекты в части своей реализации. И как именно, одним только их разработчикам ведомо, и то не факт. Если теперь весь использующий их софт вдруг начнёт использовать std::function с поддержкой объектов, использующих только семантику перемещения, один чёрт знает, как это может аукнуться даже на самих таких библиотеках, не говоря уже об этом софте.
Добавлено В общем, если это свой собственный велосипед, это ещё куда ни шло, хотя бы видно, что не std. Но в std такую фичу вводить вряд ли будут, опасно это. |
Сообщ.
#21
,
|
|
|
Цитата Qraizer @ Так я как раз об этом. Копирование и перемещение – это разные семантики. Никто с этим не спорит. Проблема в том, что там ты ничего не копируешь. Где ты видишь копирование в std::function<....> fn = std::bind(.....)? Т.е. std::function обязательно требует наличие конструктора копирования для rvalue. Зачем? Цитата Qraizer @ Если теперь весь использующий их софт вдруг начнёт использовать std::function с поддержкой объектов, использующих только семантику перемещения, один чёрт знает, как это может аукнуться даже на самих таких библиотеках, не говоря уже об этом софте. Что значит "только семантику перемещения"? Контейнеры только её используют? Чем std::function отличается от них? |
Сообщ.
#22
,
|
|
|
Цитата Олег М @ Подозреваю, что std::function реализована точно также - виртуальный метод, динамическое создание класса и т.д. Т.е. оверхед абсолютно такой же. Зачем подозревать? Просто откомпилируй и в отладчике на месте вызовов функций посмотри в Disassembly: в вызове std::function поверх обычной функции ровно те же инструкции, что и при вызове самой функции, в твоем коде инструкций больше. Оверхед небольшой и в 99+% случаев на него можно забить, но в STL стараются все оптимизировать по максимуму. |
Сообщ.
#23
,
|
|
|
Цитата Serpentus @ Зачем подозревать? Просто откомпилируй и в отладчике на месте вызовов функций посмотри в Disassembly: в вызове std::function поверх обычной функции ровно те же инструкции, что и при вызове самой функции, в твоем коде инструкций больше. Оверхед небольшой и в 99+% случаев на него можно забить, но в STL стараются все оптимизировать по максимуму. А ты std::function инициализировал через std::bind с параметрами? |
Сообщ.
#24
,
|
|
|
Цитата Олег М @ Проехали.Т.е. std::function обязательно требует наличие конструктора копирования для rvalue. Зачем? Цитата Олег М @ Контейнеры крайне редко использовались программистами в роли обычных переменных обычным для программиста способом, т.к. все понимали, что их копирование суть тяжёлая операция и всячески старались это оптимизировать, а уж для библиотечного кода и подавно. Что значит "только семантику перемещения"? Контейнеры только её используют? Чем std::function отличается от них? |
Сообщ.
#25
,
|
|
|
Цитата Qraizer @ Проехали. И что же мы проехали? Насколько мне известно, std::function появилась в 11-м стандарте, где move-семантика уже вовсю использовалась. О каком старом коде и о каких привычках программистов может идти речь? Объясни мне, почему вот этот код компилируется std::unique_ptr<std::string> sp(new std::string("!!!!!!!!!!!!!!!!!")); auto fn = [sp = std::move(sp)]() { std::cout << *sp << std::endl; }; auto fn2 = std::move(fn); fn2(); А вот этот - нет std::unique_ptr<std::string> sp(new std::string("!!!!!!!!!!!!!!!!!")); std::function<void()> fn = [sp = std::move(sp)]() { std::cout << *sp << std::endl; }; Хотя принципиального отличия лично я не вижу. Цитата Qraizer @ Контейнеры крайне редко использовались программистами в роли обычных переменных обычным для программиста способом, т.к. все понимали, что их копирование суть тяжёлая операция и всячески старались это оптимизировать, а уж для библиотечного кода и подавно. Т.е. копирование обычной переменной это считается нормальным? Или я как-то не понимаю, что значит "обычная переменная"? |
Сообщ.
#26
,
|
|
|
Цитата Олег М @ Объясни мне, почему вот этот код компилируется А вот этот - нет Хотя принципиального отличия лично я не вижу. Потому что в первом случае - некий сгенерированный компилятором класс, который (в данном случае) будет Moveable, но не CopyConstructable (согласно требованиям стандарта), а во втором - std::function, со строго специфицированным внешним поведением. Ну не может у тебя std::function быть в одном случае копируемым, а в другом - перемещаемым (в зависимости от того, чем его инициализировали). А вот лямбда-класс - может, потому что безымянный с минимально специфицированным публичным интерфейсом. |
Сообщ.
#27
,
|
|
|
Да уж. Дошло наконец-то.
Т.е. действительно, здесь без виртуального метода не обойтись. |
Сообщ.
#28
,
|
|
|
Фуф.
|
Сообщ.
#29
,
|
|
|
Цитата Олег М @ А ты std::function инициализировал через std::bind с параметрами? И так, и сяк. Двоичный код при вызове std::function идентичен коду при вызове того, чем он проинициализирован. Это ожидаемо, потому что std::function копирует внутрь себя объект, которым инициализируется, после что вызывает эту копию. (Под вызовом я имею в виду вызов "operator ()"). |
Сообщ.
#30
,
|
|
|
Цитата Serpentus @ И так, и сяк. Двоичный код при вызове std::function идентичен коду при вызове того, чем он проинициализирован. Это ожидаемо, потому что std::function копирует внутрь себя объект, которым инициализируется, после что вызывает эту копию. (Под вызовом я имею в виду вызов "operator ()"). Я думаю, если инициализация std::function и использование будут в разных единицах трансляции - то код будет немного другим, с косвенным вызовом. Но я могу ошибаться. |
Сообщ.
#31
,
|
|
|
Цитата Serpentus @ И так, и сяк. Двоичный код при вызове std::function идентичен коду при вызове того, чем он проинициализирован. Это ожидаемо, потому что std::function копирует внутрь себя объект, которым инициализируется, после что вызывает эту копию. (Под вызовом я имею в виду вызов "operator ()"). Т.е. это надо выделить память, вызвать для неё конструктор и сохранить где-то ссылки на деструктор и оператор(), так наверное? |
Сообщ.
#32
,
|
|
|
Flex Ferrum
ИМХО в случае простого указателя на функцию код все равно будет одним и тем же: "положить" параметры, загрузить в регистр адрес функции (не важно, переменная-указатель это или числовой литерал) и совершить переход. Для функциональных объектов - не знаю, возможно. Но я не проверял, так что утверждать не берусь. Олег М Так, но это на создание/удаление. А на вызов оверхед нулевой. |
Сообщ.
#33
,
|
|
|
Цитата Serpentus @ Так, но это на создание/удаление. А на вызов оверхед нулевой. В смысле, нулевой? Я так понимаю - нулевой, это когда inline. А здесь будет вызов обычного метода. |
Сообщ.
#34
,
|
|
|
Олег М
В смысле по сравнению с вызовом того, чем проинициализировали std::function. |
Сообщ.
#35
,
|
|
|
Цитата Serpentus @ В смысле по сравнению с вызовом того, чем проинициализировали std::function. Ну да, так и должно быть. Тут вот тоже интересно - как в bind эти placeholders разруливаются, ещё одно tuple строится, со ссылками? Или как-то по-другому? |