Идеальная пересылка вызываемого
Я придумал следующий код для преобразования R()
-like в функцию void()
-like:
#include <utility>
template<class Callable>
auto discardable(Callable&& callable)
{ return [&]() { (void) std::forward<Callable>(callable)(); }; }
// ^-- is it ok?
int main()
{
auto f = discardable([n=42]() mutable { return n--; });
f();
}
Я беспокоюсь о захвате по ссылке.
- Это четко определено?
- Я гарантированно, что
callable
никогда не копируется и никогда не используется после окончания его жизни?
Это помечено С++ 14, но применяется ко всем следующим стандартам.
Ответы
Ответ 1
Лямбды - это анонимные структуры с operator()
, список перехвата - причудливый способ указать тип его членов. Захват по ссылке действительно таков: у вас есть референтные члены. Это не трудно увидеть ссылки свисает.
Это тот случай, когда вы специально не хотите совершать прямую пересылку: у вас разная семантика в зависимости от того, является ли аргумент ссылкой на lvalue или rvalue.
template<class Callable>
auto discardable(Callable& callable)
{
return [&]() mutable { (void) callable(); };
}
template<class Callable>
auto discardable(Callable&& callable)
{
return [callable = std::forward<Callable>(callable)]() mutable { // move, don't copy
(void) std::move(callable)(); // If you want rvalue semantics
};
}
Ответ 2
Поскольку callable
может быть значением xvalue, есть вероятность, что он будет уничтожен до лямбда-захвата, и, следовательно, у вас останется свисающая ссылка в захвате. Чтобы предотвратить это, если аргумент является r-значением, его необходимо скопировать.
Рабочий пример:
template<class Callable>
auto discardable(Callable&& callable) { // This one makes a copy of the temporary.
return [callable = std::move(callable)]() mutable {
static_cast<void>(static_cast<Callable&&>(callable)());
};
}
template<class Callable>
auto discardable(Callable& callable) {
return [&callable]() mutable {
static_cast<void>(callable());
};
}
Вы все еще можете столкнуться с проблемами времени жизни, если callable
является ссылкой на l-значение, но его область действия меньше, чем объем лямбда-захвата, возвращаемого discardable
. Таким образом, это может быть самый безопасный и простой способ всегда перемещать или копировать callable
.
В качестве примечания, хотя есть новые специализированные утилиты, которые совершают прямую пересылку в категорию значений объекта функции, например std::apply
, стандартные алгоритмы библиотеки всегда копируют объекты функции, принимая их по значению. Таким образом, если кто-то перегружен как operator()()&
и operator()()&&
стандартная библиотека всегда будет использовать operator()()&
.
Ответ 3
Ваша программа UB, так как вы используете висячую ссылку захваченной лямбды.
Так что для идеального захвата вперед в лямбде, вы можете использовать
template<class Callable>
auto discardable(Callable&& callable)
{
return [f = std::conditional_t<
std::is_lvalue_reference<Callable>::value,
std::reference_wrapper<std::remove_reference_t<Callable>>,
Callable>{std::forward<Callable>(callable)}]
{
std::forward<Callable>(f)();
};
}
Он двигает-строит временную лямбду.
Ответ 4
[&]() { }
// ^-- is it ok?
Да.
Затем вы задаете неправильные вопросы, чтобы ответы были отрицательными. Однако код совершенно в порядке. Это то, что я имею в виду:
Я гарантированно, что вызываемый никогда не копируется
Номер callable
является ссылкой для пересылки. Если вы передадите lvalue, оно будет скопировано. Если вы передадите значение, оно будет перемещено. Так работают пересылки ссылок и std::forward
. Однако гарантируется, что callable
всегда будет перенаправлен, как ожидается.
... и никогда не использовался после его жизни?
Нет. Но гарантируется, что discardable
не вернет висячую ссылку (если, конечно, она получила действительную ссылку). Но нет никакой гарантии, что вы не пропустите ссылку в остальной части программы.
Ответ 5
Вероятно, должно быть:
return [callable = std::forward<Callable>(callable)]() {
(void) std::forward<Callable>(callable)();
};