среда, 21 декабря 2011 г.

Лямбды в C++11 - тонкости использования

Переходя от теории к практике, при применении лямбд со списками захвата надо соблюдать известную осторожность. Вызвано это особенностями их реализации, прописанными в стандарте. А именно: лямбда-функция, имеющая список, захвата, это (по своей сути) экземпляр локального класса, объявленного внутри той функции/блока, где она определяется. Выход за границы этого блока череват тем, что некоторые захваченные переменные станут невалидными. И если для простых случаев, когда лямбда просто возвращается из метода, всё более-менее очевидно, то для случаев, когда лямбда-функция (её экземпляр) сохраняется в теле класса, могут быть неожиданности.


Достаточно простой пример:

  1 class SomeClass
  2 {
  3 public:
  4    SomeClass()
  5    {
  6       m_SomeFunctor = [this]() {this->Foo();}
  7    }
  8 
  9    void Foo() {;}
 10    void Bar() {m_SomeFunctor();}
 11 
 12 private:
 13    std::function<void ()> m_SomeFunctor;
 14 };
 15 
 16 SomeClass MakeClassInstance()
 17 {
 18    SomeClass cls;
 19 
 20    cls->Foo();
 21 
 22    return cls;
 23 }
 24 
 25 //...
 26 
 27 SomeClass cls = MakeSomeClass();
 28 cls.Bar(); // oops!!!

Тут происходит одна интересная, но неочевидная (на первый взгляд) вещь. В конструкторе создаётся лямбда-функция, в списке захвата которой находится this. Экземпляр класса создаётся с помощью порождающей функции (MakeClassInstance), после чего в строке 28 происходит неприятность. Из за того, что работаем мы, фактически, с копией созданного внутри MakeClassInstance объекта, указатель на SomeClass, сохранённый внутри лямбда-функции, является невалидным.

Будьте осторожнее!

7 комментариев:

  1. да, с++ в своем репертуаре...

    вместе с тортиком в виде лямбды поставляются кусочки стекла, так что поедание тортика требует высокого профессионализма :-)

    иногда можно исправить так:

    const SomeClass& cls = MakeSomeClass();

    кстати, тут RVO вроде применимо? если сработает, то this останется валидным, так?

    p.s. а нафига у тебя капча для комментаторов?

    ОтветитьУдалить
  2. Я не к тому клоню, что "с RVO все будет хорошо", а наоборот -- с RVO можно этот баг и не заметить

    ОтветитьУдалить
  3. имя, ну, можно и так сказать. С другой стороны - такими "кусочками стекла" весь C++ нашпигован по самое небалуйся. Возможностей отстрелить себе ногу - хоть отбавляй. Просто надо быть аккуратнее. Да и такая особенность лямбды напрямую вытекает из общей архитектуры языка.
    И таки да - RVO может скрыть этот косяк.

    ЗЫ: Каптчу отключил.

    ОтветитьУдалить
  4. насчет "общей архитектуры языка" -- понятно, про эффективность

    я так понимаю, лямбды создавались под локальное использование, а нелокальные лямбды, кмк, нужно создавать так, чтобы захваченные аргументы копировались -- в самом деле, мы ведь не собираемся организовывать скрытые каналы связи и прочую лабуду из серии "сэмулируем ооп на лямбдах"?

    если они будут копироваться, то и проблем не будет

    только вот мне не ясно, как это сделать в твоем случае... и вообще не ясно, что планировалось

    ОтветитьУдалить
  5. еще раз -- назначение SomeClass не ясно

    я конечно подозреваю, что SomeClass предполагался, например, как сумматор, из которого можно достать сумму, и который для удобства сразу предлагает нужную для суммирования лямбду... но лучше ты сам напиши

    ОтветитьУдалить
  6. а еще мне сильно кажется, что вообще RVO должно быть не оптимизацией, а единственным возможным вариантом, если RVO возможно

    ОтветитьУдалить
  7. "я так понимаю, лямбды создавались под локальное использование, а нелокальные лямбды, кмк, нужно создавать так, чтобы захваченные аргументы копировались -- в самом деле, мы ведь не собираемся организовывать скрытые каналы связи и прочую лабуду из серии "сэмулируем ооп на лямбдах"?"
    Не обязательно. И не только для этого. Например:

    class Dispatcher
    {
    //...
    boost::signal OnSomething;
    //...
    };

    void SomeClass::Subscribe(Dispatcher* disp)
    {
    disp->OnSomething.connect([this]() {DoSomething();}
    }

    Вполне себе нелокальная лямбда.

    "я конечно подозреваю, что SomeClass предполагался, например, как сумматор, из которого можно достать сумму, и который для удобства сразу предлагает нужную для суммирования лямбду... но лучше ты сам напиши"
    Примерно так, только не сумматор, а что-то вроде projection iterator. В шаблонном конструкторе создаётся лямбда, заточенная на итерирование конкретного типа последовательности. Часть стейта лямбда хранит во внешнем классе (который и передаётся по this). По-хорошему, перепроектировать надо. Но тут надо будет хорошо голову поломать, потому что лишний раз играться с динамической памятью тоже не хочется.

    "а еще мне сильно кажется, что вообще RVO должно быть не оптимизацией, а единственным возможным вариантом, если RVO возможно "
    :) Ну да. :)

    ОтветитьУдалить