Генерація коду лямбда C ++ із захопленням Ініта в C ++ 14


9

Я намагаюся зрозуміти / уточнити код коду, який генерується при передачі захоплень лямбдам, особливо в узагальнених захопленнях init, доданих в C ++ 14.

Наведіть наведені нижче зразки коду. Це моє поточне розуміння того, що буде створювати компілятор.

Випадок 1: захоплення за значенням / захоплення за замовчуванням за значенням

int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };

Зрівняється з:

class __some_compiler_generated_name {
public:
    __some_compiler_generated_name(int x) : __x{x}{}
    void operator()() const { std::cout << __x << std::endl;}
private:
    int __x;
};

Отже, існує декілька копій: одна для копіювання в параметр конструктора і друга для копіювання в член, що було б дорого для таких типів, як векторні тощо.

Випадок 2: захоплення посиланням / захоплення за замовчуванням за посиланням

int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };

Зрівняється з:

class __some_compiler_generated_name {
public:
    __some_compiler_generated_name(int& x) : x_{x}{}
    void operator()() const { std::cout << x << std::endl;}
private:
    int& x_;
};

Параметр є посиланням, а член - посиланням, тому копій немає. Підходить для таких типів, як векторні тощо.

Випадок 3:

Узагальнений захоплення init

auto lambda = [x = 33]() { std::cout << x << std::endl; };

Я вважаю, що це схоже на випадок 1 в тому сенсі, що він скопійований в учасника.

Я здогадуюсь, що компілятор генерує код, схожий на ...

class __some_compiler_generated_name {
public:
    __some_compiler_generated_name() : __x{33}{}
    void operator()() const { std::cout << __x << std::endl;}
private:
    int __x;
};

Також якщо у мене є таке:

auto l = [p = std::move(unique_ptr_var)]() {
 // do something with unique_ptr_var
};

Як виглядав би конструктор? Це також переміщує його в член?


1
@ rafix07 У цьому випадку згенерований код прозорливості навіть не компілюється (він намагається скопіювати-ініціалізувати унікальний член ptr з аргументу). cppinsights корисний для отримання загальної суті, але тут явно не в змозі відповісти на це питання.
Макс Ленгоф

Ви начебто припускаєте, що є переклад лямбда на функтори як перший крок компіляції, або ви просто шукаєте еквівалентний код (тобто таку саму поведінку)? Те, як конкретний компілятор генерує код (і який код він генерує), залежатиме від компілятора, версії, архітектури, прапорів тощо. Отже, ви запитуєте конкретну платформу? Якщо ні, то ваше запитання насправді не відповідає. Окрім фактично створеного коду, ймовірно, буде ефективніше, ніж перелічені вами функції (наприклад, вбудовані конструктори, уникаючи зайвих копій тощо).
Сандер Де

2
Якщо вас цікавить, що стандарт C ++ має про це сказати, зверніться до [expr.prim.lambda] . Це занадто багато, щоб узагальнити тут відповідь.
Сандер Де

Відповіді:


2

На це питання не можна повністю відповісти в коді. Можливо, ви зможете написати дещо "еквівалентний" код, але стандарт не вказаний таким чином.

Коли це не вийде, давайте зануримось [expr.prim.lambda]. Перше, що слід зазначити, конструктори згадуються лише у [expr.prim.lambda.closure]/13:

Тип закриття, пов'язаний з лямбда-виразом , не має конструктора за замовчуванням, якщо лямбда-вираз має ламбда-захоплення, а конструктор за замовчуванням за замовчуванням - в іншому випадку. У ній є конструктор копій, що не використовується, та конструктор переміщення за умовчанням ([class.copy.ctor]). У нього є видалений оператор присвоєння копії, якщо лямбда-вираз має лямбда-захоплення та дефолт, копіюючи та переміщуючи оператори присвоєння інакше ([class.copy.assign]). [ Примітка: Ці спеціальні функції членів неявно визначені як зазвичай, і тому можуть бути визначені як видалені. - кінцева примітка ]

Тож одразу ж у біту має бути зрозуміло, що конструктори формально не визначають спосіб захоплення об'єктів. Ви можете наблизитись (див. Відповідь cppinsights.io), але деталі відрізняються (зауважте, як код у цій відповіді для випадку 4 не компілюється).


Це основні стандартні пункти, необхідні для обговорення випадку 1:

[expr.prim.lambda.capture]/10

[...]
Для кожного об'єкта, захопленого копією, неназваний нестатичний член даних оголошується у типі закриття. Порядок декларування цих членів не визначений. Тип такого члена даних є посилальним типом, якщо суб'єктом є посилання на об'єкт, значення посилання на значення типу посилається, якщо сутність є посиланням на функцію, або тип відповідного захопленого об'єкта в іншому випадку. Член анонімного союзу не може бути захоплений копією.

[expr.prim.lambda.capture]/11

Кожен вираз id у складеному операторі лямбда-виразу, який є odr-використанням об'єкта, захопленого копією, перетворюється на доступ до відповідного неназваного члена даних типу закриття. [...]

[expr.prim.lambda.capture]/15

Коли оцінюється лямбда-вираз, об'єкти, захоплені копією, використовуються для прямої ініціалізації кожного відповідного нестатичного члена даних об'єкта закриття, а нестатичні члени даних, що відповідають init-captures, ініціалізуються як позначається відповідним ініціалізатором (який може бути ініціалізацією копіювання чи прямої). [...]

Давайте застосуємо це до вашої справи 1:

Випадок 1: захоплення за значенням / захоплення за замовчуванням за значенням

int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };

Тип закриття цієї лямбда матиме неназваний нестатичний член даних (назвемо це __x) типу int(оскільки xце не є посиланням і не функцією), а доступ до xтіла лямбда перетворюється на доступ до __x. Коли ми оцінюємо лямбда-вираз (тобто при призначенні lambda), ми пряме-ініціалізуємо __x з x.

Словом, має місце лише одна копія . Конструктор типу закриття не задіяний, і це неможливо виразити "нормальним" C ++ (зауважте, що тип закриття також не є агрегованим типом ).


Захоплення рефератів включає [expr.prim.lambda.capture]/12:

Суб'єкт охоплюється за посиланням, якщо він неявно або явно захоплений, але не зафіксований копією. Не визначено, чи оголошуються додаткові неназвані нестатичні члени даних у типі закриття для об'єктів, захоплених посиланням. [...]

Є ще один абзац щодо збору посилань, але ми цього ніде не робимо.

Отже, для випадку 2:

Випадок 2: захоплення посиланням / захоплення за замовчуванням за посиланням

int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };

Ми не знаємо, чи додається член до типу закриття. xв тілі лямбда може просто безпосередньо посилатися на xзовнішню сторону. Це залежить від компілятора, і він зробить це в якійсь формі проміжної мови (яка відрізняється від компілятора до компілятора), а не в вихідному перетворенні коду C ++.


Захоплення Init детально описано у [expr.prim.lambda.capture]/6:

Захоплення init поводиться так, ніби він оголошує і явно фіксує змінну форми auto init-capture ;, декларативною областю якої є сполука-вираз лямбда-виразу, за винятком того, що:

  • (6.1) якщо захоплення здійснюється копією (див. Нижче), нестатичний член даних, оголошений для захоплення, та змінна трактуються як два різні способи посилання на один і той же об'єкт, який має термін експлуатації нестатичних даних член, і додаткові копії та знищення не виконуються, і
  • (6.2) якщо захоплення відбувається за посиланням, час змінної закінчується, коли закінчується термін експлуатації об'єкта закриття.

Враховуючи це, давайте розглянемо випадок 3:

Випадок 3: Узагальнений захоплення init

auto lambda = [x = 33]() { std::cout << x << std::endl; };

Як зазначено, уявіть це як змінну, яка створюється auto x = 33;копією та явно фіксується. Ця змінна є "видимою" лише в тілі лямбда. Як зазначалося [expr.prim.lambda.capture]/15раніше, ініціалізація відповідного члена типу закриття ( __xдля нащадків) здійснюється даним ініціалізатором після оцінки виразу лямбда.

Щоб уникнути сумнівів: це не означає, що тут ініціалізуються двічі. Це auto x = 33;"як би" успадковувати семантику простих фіксацій, а описана ініціалізація є модифікацією цієї семантики. Буває лише одна ініціалізація.

Це стосується також випадку 4:

auto l = [p = std::move(unique_ptr_var)]() {
  // do something with unique_ptr_var
};

Член типу закриття ініціалізується __p = std::move(unique_ptr_var)тоді, коли оцінюється вираз лямбда (тобто коли lпризначено). Доступи до pтіла лямбда перетворюються на доступ до __p.


TL; DR: Виконується лише мінімальна кількість копій / ініціалізації / ходи (як можна було б сподіватися / очікувати). Я б припустив, що лямбда не визначені з точки зору трансформації джерела (на відміну від інших синтаксичних цукрів) саме тому , що вираження речей через конструктори потребує зайвих операцій.

Я сподіваюся, що це вирішує побоювання, висловлені у запитанні :)


9

Випадок 1 [x](){} : Створений конструктор прийме свій аргумент, можливо, constкваліфікованою посиланням, щоб уникнути зайвих копій:

__some_compiler_generated_name(const int& x) : x_{x}{}

Випадок 2 [x&](){} : Ваші припущення тут правильні, xпередаються та зберігаються за посиланням.


Випадок 3 [x = 33](){} : Знову правильно, xініціалізується значенням.


Випадок 4 [p = std::move(unique_ptr_var)] : Конструктор буде виглядати так:

    __some_compiler_generated_name(std::unique_ptr<SomeType>&& x) :
        x_{std::move(x)}{}

так що так, unique_ptr_var"закривається" закриття. Дивіться також Пункт 32 Скотта Мейєра в "Ефективній сучасній C ++" ("Використовувати захоплення init для переміщення об'єктів у закриття").


" const-кваліфікований" Чому?
cpplearner

@cpplearner Mh, гарне запитання. Я думаю, що я вставив це через те, що один із тих психічних автоматизмів забився в ^^ Принаймні, constтут не може поранитися через деяку неоднозначність / кращу відповідність, коли не є const. Інакше, ти вважаєш, що мені слід зняти const?
лубгр

Я думаю, що const повинен залишатися, що, якщо аргумент, переданий фактично, це const?
Аконкагуа

Отже, ви кажете, що тут відбуваються дві рухомі (або копіювальні) конструкції?
Макс Лангхоф

Вибачте, я маю на увазі у випадку 4 (для рухів) та випадок 1 (для копій). Копіювати частину мого запитання не має сенсу, виходячи з ваших тверджень (але я ставлю під сумнів ці твердження).
Макс Лангхоф

5

Не потрібно спекулювати, використовуючи cppinsights.io .

Випадок 1:
Кодекс

#include <memory>

int main() {
    int x = 33;
    auto lambda = [x]() { std::cout << x << std::endl; };
}

Компілятор генерує

#include <iostream>

int main()
{
  int x = 6;

  class __lambda_5_16
  {
    int x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_5_16(const __lambda_5_16 &) = default;
    // inline /*constexpr */ __lambda_5_16(__lambda_5_16 &&) noexcept = default;
    public: __lambda_5_16(int _x)
    : x{_x}
    {}

  };

  __lambda_5_16 lambda = __lambda_5_16(__lambda_5_16{x});
}

Випадок 2:
Кодекс

#include <iostream>
#include <memory>

int main() {
    int x = 33;
    auto lambda = [&x]() { std::cout << x << std::endl; };
}

Компілятор генерує

#include <iostream>

int main()
{
  int x = 6;

  class __lambda_5_16
  {
    int & x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_5_16(const __lambda_5_16 &) = default;
    // inline /*constexpr */ __lambda_5_16(__lambda_5_16 &&) noexcept = default;
    public: __lambda_5_16(int & _x)
    : x{_x}
    {}

  };

  __lambda_5_16 lambda = __lambda_5_16(__lambda_5_16{x});
}

Випадок 3:
Кодекс

#include <iostream>

int main() {
    auto lambda = [x = 33]() { std::cout << x << std::endl; };
}

Компілятор генерує

#include <iostream>

int main()
{

  class __lambda_4_16
  {
    int x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_4_16(const __lambda_4_16 &) = default;
    // inline /*constexpr */ __lambda_4_16(__lambda_4_16 &&) noexcept = default;
    public: __lambda_4_16(int _x)
    : x{_x}
    {}

  };

  __lambda_4_16 lambda = __lambda_4_16(__lambda_4_16{33});
}

Справа 4 (неофіційно):
Кодекс

#include <iostream>
#include <memory>

int main() {
    auto x = std::make_unique<int>(33);
    auto lambda = [x = std::move(x)]() { std::cout << *x << std::endl; };
}

Компілятор генерує

// EDITED output to minimize horizontal scrolling
#include <iostream>
#include <memory>

int main()
{
  std::unique_ptr<int, std::default_delete<int> > x = 
      std::unique_ptr<int, std::default_delete<int> >(std::make_unique<int>(33));

  class __lambda_6_16
  {
    std::unique_ptr<int, std::default_delete<int> > x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x.operator*()).operator<<(std::endl);
    }

    // inline __lambda_6_16(const __lambda_6_16 &) = delete;
    // inline __lambda_6_16(__lambda_6_16 &&) noexcept = default;
    public: __lambda_6_16(std::unique_ptr<int, std::default_delete<int> > _x)
    : x{_x}
    {}

  };

  __lambda_6_16 lambda = __lambda_6_16(__lambda_6_16{std::unique_ptr<int, 
                                                     std::default_delete<int> >
                                                         (std::move(x))});
}

І я вважаю, що цей останній фрагмент коду відповідає на ваше запитання. Переміщення відбувається, але не [технічно] в конструкторі.

Самих знімків немає const, але ви можете бачити, що ця operator()функція є. Природно, якщо вам потрібно змінити захоплення, ви позначите лямбда як mutable.


Код, показаний для останнього випадку, навіть не компілюється. Висновок "переміщення відбувається, але не [технічно] в конструкторі" не може бути підтримане цим кодом.
Макс Лангхоф

Код випадку 4 безумовно компілюється на моєму Mac. Я здивований, що згенерований розширений код з cppinsights не компілюється. На даний момент сайт був для мене досить надійним. Я підніму питання з ними. EDIT: я підтвердив, що згенерований код не компілюється; це не було зрозуміло без цього редагування.
Soundsish

1
Посилання на проблему у разі зацікавлення: github.com/andreasfertig/cppinsights/isissue/258 Я все-таки рекомендую веб-сайту такі речі, як тестування SFINAE та незалежно від того, чи відбуватимуться неявні касти.
Soundsish
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.