#опытным
В недавнем посте мы рассказали о проблеме, когда лямбда, захватывающая объект по ссылке, возвращается из метода. Это потенциально может привести к тому, что объект уничтожится раньше, чем произойдет вызов лямбды, что в итоге приведет к провисшей ссылке, а значит - к UB.
struct Task {
int id;
std::function<void()> GetNotifier() {
return [&]{
std::cout << "notify " << id << std::endl;
};
}
};
int main() {
auto notify = Task { 5 }.GetNotifier();
notify();
}Можно вместо захвата по ссылке использовать захват по значению. Но скорее всего это не поможет, так как хочется использовать тот же самый объект, из метода которого возвращалась лямбда.
Тут бы шаред поинтер использовать. Но втупую его заюзать тоже ничем не поможет:
struct Task {
int id;
std::function<void()> GetNotifier() {
return [curr = std::shared_ptr<Task>(this)]{
std::cout << "notify " << curr->id << std::endl;
};
}
};
int main() {
auto notify = Task { 5 }.GetNotifier();
notify();
}Да, мы создали из this шареный указатель, но его контрольный блок никак не учитывает оригинальный объект!
curr будет думать, что только он и его копии будут владеть объектом, а оригинальный объект просто уничтожится и curr будет ссылаться на невалидный объект. Получили то же самое UB.Что делать?
Использовать миксин C++11 std::enable_shared_from_this. Это базовый CRTP класс, который предоставляет метод shared_from_this(). Если вы обернете исходный объект в std::shared_ptr, то метод shared_from_this возвращает копию объекта умного указателя, в котором находился исходный объект. Эта копия будет разделять контрольный блок с оригинальным объектом, поэтому пока жива хоть одна копия, исходный объект не разрушится. Выглядит это так:
struct Task : std::enable_shared_from_this<Task> {
int id;
std::function<void()> GetNotifier() {
// Захватываем shared_ptr на текущий объект
return [self = shared_from_this()] {
std::cout << "notify " << self->id << std::endl;
};
}
};
int main() {
auto notify = std::make_shared<Task>(5)->GetNotifier();
notify(); // Теперь безопасно - объект не будет уничтожен
}
В первой строчке main происходит много вещей:
1️⃣ Создается объект класса Task и оборачивается во временный объект умного указателя.
2️⃣ Вызывается метод GetNotifier у объекта внутри временного умного указателя, из которого возвращается лямбда с захваченной копией временного объекта.
3️⃣ До перехода к следующей строчке временный объект, созданный через make_shared, разрушается.
Но ничего страшного не происходит, потому что notify хранит в себе копию временного объекта умного указателя, а значит тебе эта лямбда владелец исходного объекта Task{5}. Поэтому при вызове этой лямбды никакого Ub и провисания ссылки нет.
Вообще, наверное пора рассказывать про CRTP, миксины и прочие нечисти шаблонов в С++. Если хотите такого, то жмякните лайкосик, а с нам посты по этим темам.
Watch your lifetime. Stay cool.
#template #cpp11