template<typename U>
struct Scheduler {
// ...
void Add(U&& callback) {
callback_ = std::move(callback);
}
// ...
U callback_;
};
И это какая-то иллюстрация Эффекта Манделлы, потому что часто на вопрос "что не так?" слышу простое, легкое для понимания, неправильное решение в духе
forwarding reference passed to std::move(), which may unexpectedly cause lvalues to be moved; use std::forward() instead
На самом деле, использование std::forward тут нежелательно.
Давайте разберем сразу 4 примера:
// 1
template<typename U>
struct Scheduler {
// ...
void Add(U&& callback) {
callback_ = std::move(callback);
}
// ...
U callback_;
};
// 2
template<typename U>
struct Scheduler {
// ...
void Add(U&& callback) {
callback_ = std::forward<U>(callback);
}
// ...
U callback_;
};
// 3
struct AnotherScheduler {
// ...
template<typename U>
void Add(U&& callback) {
callback_ = std::move(callback);
}
// ...
std::function<void()> callback_;
};
// 4
struct AnotherScheduler {
// ...
template<typename U>
void Add(U&& callback) {
callback_ = std::forward<U>(callback);
}
// ...
std::function<void()> callback_;
};
В первом примере все хорошо: тип зафиксирован в момент объявления инстанса класса
Scheduler<std::function<void()>> sh;
поэтому в объявлении функции U&& -> это rvalue reference. И поскольку rvalue reference is lvalue, мы должны его мувать. Если использовать std::forward (Как в примере номер 2), то ничего страшного не случиться, но будет выглядеть странно, и заставит ругаться clang-tidy. Возможно, будут какие-то гадости, которые я не смог воспроизвести.
В примерах 3-4 все наоборот. Тип U определяется на этапе вызова функции, и его мувать опасно: у нас аргумент функции - forwarding reference, а значит там может быть как rvalue, так и просто ссылка. Если вы муваете ссылку внутри функции, тот, кто ее вызывает, может внезапно обнаружить, что переданный объект, который он не мувал, изчез:
AnotherScheduler ash;
auto af = []() -> void {};
ash.Add(std::ref(af)); // moved here inside function
af(); // UB
А потому пример 3 - неверный и потенциальный источник багов. А пример 4 - норм.
Итого, "корректные" решения - 1 и 4. И, как и положено в С++, примеры 2 и 3 нормально скомпилируются... И даже будут как-то работать, наверное, в большинстве случаев, но делать так не надо.
И вообще, читаем cpp core guidelines.