Идеальная пересылка вызываемого

Я придумал следующий код для преобразования 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();
}

Я беспокоюсь о захвате по ссылке.

  1. Это четко определено?
  2. Я гарантированно, что 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)(); 
};