У меня есть вопрос о сохранении объекта с лямбда-шаблоном в качестве члена класса. Класс Invoker — это шаблонный класс, хранящий произвольную лямбда-функцию. Я хочу сохранить экземпляр Invoker в другом классе, Worker. Однако я не знаю, как заполнить аргумент шаблона TCallback, когда Invoker используется как член класса. Он не выводится, как первая строка в функции main. Как показано в комментариях, я пытаюсь определить лямбду где-то в Worker и передать ее члену типа Invoker.
Что я пробовал, так это использовать decltype метода класса, но его нельзя было вызвать как общую лямбду - для запуска ему нужен контекст объекта класса.
Большое спасибо за любые идеи и, возможно, некоторые обходные пути.
Спасибо.
#include <iostream>
template <typename TCallback>
struct Invoker {
explicit Invoker(TCallback&& cb) : cb(cb) {}
TCallback cb;
void invoke() {
cb();
}
};
struct Worker {
void some_callback() {
std::cout << "callback in worker\n";
}
// Not working: some_callback is a member function and can only be called with the context object
Invoker<decltype(&Worker::some_callback)> invoker{&Worker::some_callback};
// How to make a something like this?
// auto lambda_in_class = [&]{some_callback()};
// Invoker<decltype(lambda_in_class)> invoker{lambda_in_class};
};
int main() {
Invoker invoker([]{std::cout << "invoker\n";});
Worker worker;
worker.invoker.invoke();
return 0;
}
Вам действительно нужно использовать ремплейты? Это работает:
#include <iostream>
#include <functional>
struct Invoker {
explicit Invoker(std::function<void()> cb) : cb(cb) {}
std::function<void()> cb;
void invoke() {
cb();
}
};
struct Worker {
void some_callback() {
std::cout << "callback in worker\n";
}
// Not working: some_callback is a member function and can only be called with the context object
Invoker invoker{std::bind(&Worker::some_callback, this)};
// How to make a something like this?
std::function<void()> lambda_in_class = [&]{some_callback();};
Invoker invoker1{lambda_in_class};
};
int main() {
Invoker invoker([]{std::cout << "invoker\n";});
Worker worker;
worker.invoker.invoke();
worker.invoker1.invoke();
return 0;
}
Вы также можете сделать это с шаблонами, но вам понадобится руководство по выводу шаблонов (которое является функцией С++ 17) для лямбда-выражений:
#include <iostream>
#include <functional>
template <typename TCallback>
struct Invoker {
explicit Invoker(TCallback cb) : cb(cb) {}
TCallback cb;
void invoke() {
cb();
}
};
template<typename T>
Invoker(T) -> Invoker<T>;
struct Worker {
void some_callback() {
std::cout << "callback in worker\n";
}
// Not working: some_callback is a member function and can only be called with the context object
Invoker<decltype(std::bind(&Worker::some_callback, static_cast<Worker *>(0)))> invoker{std::bind(&Worker::some_callback, this)};
// How to make a something like this?
// auto lambda_in_class = [&]{some_callback()};
// Invoker<decltype(lambda_in_class)> invoker{lambda_in_class};
};
int main() {
Invoker invoker([]{std::cout << "invoker\n";});
Worker worker;
worker.invoker.invoke();
return 0;
}
Лямбда внутри класса будет работать только с std::function, так как невозможно использовать auto для члена класса, и вы не можете определить его тип каким-либо другим способом.
Для функций-членов, чтобы упростить их использование, вы даже можете создать специальный конструктор и руководство по выводу. Так:
// Add constructor to Invoker definition
template<typename T>
Invoker(void (T::*func)(), T* obj) : cb(std::bind(func, obj)) {}
// Add template guide after Invoker definition
template<typename T>
Invoker(void (T::*func)(), T* obj) -> Invoker<decltype(std::bind(func, obj))>;
// Now you can declare invoker in Worker like this
decltype(Invoker(&Worker::some_callback, static_cast<Worker *>(0))) invoker{&Worker::some_callback, this};
// Or outside, like this
Worker worker;
auto invoker = Invoker{&Worker::some_callback, &worker};
@YuanyiWu Добавил несколько вариантов...
Отличный пример! Два вопроса: 1. код не компилировался в Visual Studio из-за C2665; 2. Я не знаком с руководством по дедукции. Кажется, если я уберу руководство по дедукции template<typename T> Invoker(T) -> Invoker<T>;, оно все равно скомпилируется и запустится. Здесь это требуется?
Еще один вопрос не по теме: кажется, компилятор способен вывести спецификации типов. Есть идеи, почему эта функция не включена для участников класса, и нам приходится писать так много стандартных кодов?
1. Что-то странное происходит с MSVC, почему-то у него const this при инициализации invoker. Таким образом, вы можете либо добавить актерский состав const_cast<Worker *>(this), либо использовать второе руководство по дедукции. 2. Это руководство по дедукции было для внеклассовой лямбды. Без него не получалось.
Любая идея, почему эта функция не включена? Я понятия не имею, но если мне придется угадывать, вероятно, чтобы не вывести неправильный тип из значения по умолчанию...
Проблема с:
Invoker<decltype(&Worker::some_callback)> invoker{&Worker::some_callback};
Заключается в том, что some_callback является нестатической функцией-членом. Когда вы вызываете его, ему фактически передается неявный указатель this. Поэтому, когда вы вызываете его через указатель функции-члена, требуется объект Worker (который не существует в вашем классе Invoker.
Одним из обходных путей является использование выражения привязки для привязки указателя this к some_callback:
std::bind(&Worker::some_callback, this)
Затем тип результата можно преобразовать в std::function<void()> и сохранить в Worker.invoker:
Invoker<std::function<void()>> invoker{std::bind(&Worker::some_callback, this)};
Демо
Invoker<std::function<void()>> invoker{&Worker::some_callback};
Демо
Спасибо! Привязка функции-члена к шаблону вызывающей стороны была лишь одной из моих попыток. Моя цель — привязать произвольную лямбду к шаблону Invoker, а также сохранить Invoker как член (без использования std::function). Как вы думаете, это возможно?
@YuanyiWu Только один тип может быть связан с invoker, поскольку точный тип invoker не может измениться. Чтобы связать несколько типов, лучше всего сделать Worker класс шаблона.
@YuanyiWu Также обратите внимание, что, основываясь на вашем Invoker, cb не может принимать никаких аргументов, и любые возвращаемые значения отбрасываются, поэтому любые переданные ему функции фактически являются типом void().
Да. Обратный вызов является концептуальным, поэтому его подпись здесь не имеет большого значения. В конце концов, фактический тип лямбды является частным для компилятора.
Что касается предложения сделать класс Worker шаблоном, я на самом деле не хочу привязываться несколько раз к обратному вызову. Я хочу сохранить обратный вызов только один раз (при инициализации, возможно, в конструкторе). Проблема в том, что тип шаблона Invoker не подлежит вычету.
Я ожидаю какой-то псевдокод, например закомментированное лямбда-объявление в примере кода. Это выглядит просто, но кажется очень недостижимым.
@YuanyiWu Проблема с вашим прокомментированным кодом не в том, что тип invoker нельзя вывести, а в том, что у lambda_in_class нет типа. Значение по умолчанию члена не предоставляет информацию о типе самому члену. Он только предоставляет информацию о том, как они инициализируются.
@YuanyiWu Вместо auto lambda_in_class = ... вы должны вручную указать конкретный тип, например function<void()>. Демо
Спасибо! использование std::function определенно выполнимо, но я также хотел бы узнать, как справиться с этой ситуацией, когда вывод типа не может работать для члена класса.