Генерация лямбда-кода на C ++ с помощью Init Capture на C ++ 14


9

Я пытаюсь понять / уточнить код кода, который генерируется, когда перехваты передаются лямбдам, особенно в обобщенных перехватах инициализации, добавленных в 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;
};

Таким образом, существует несколько копий, одну для копирования в параметр конструктора и одну для копирования в элемент, что будет дорого для таких типов, как vector и т. Д.

Случай 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:

Обобщенный захват инициализации

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

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

[expr.prim.lambda.capture]/11

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

[expr.prim.lambda.capture]/15

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

Давайте применим это к вашему случаю 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 ++.


Захваты инициации подробно описаны в [expr.prim.lambda.capture]/6:

Захват init ведет себя так, как будто он объявляет и явно захватывает переменную формы, auto init-capture ;чья декларативная область является составным оператором лямбда-выражения, за исключением того, что:

  • (6.1) если перехват производится копией (см. Ниже), элемент нестатических данных, объявленный для перехвата, и переменная рассматриваются как два разных способа обращения к одному и тому же объекту, у которого есть время жизни нестатических данных. член, и никакое дополнительное копирование и уничтожение не выполняется, и
  • (6.2) если захват выполняется по ссылке, время жизни переменной заканчивается, когда заканчивается время жизни объекта замыкания.

Учитывая это, давайте посмотрим на случай 3:

Случай 3: Обобщенный захват инициализации

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 Скотта Мейера в Effective Modern C ++ («Используйте захват init для перемещения объектов в замыкания»).


« constКвалифицированный» Почему?
Cpplearner

@cpplearner Мх, хороший вопрос. Я предполагаю, что вставил это, потому что один из тех умственных автоматов включился в ^^ По крайней мере, constздесь не может быть больно из-за некоторой неоднозначности / лучшего соответствия, когда не constи т. Д. В любом случае, вы думаете, я должен удалить const?
Lubgr

Я думаю, что 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 не компилируется. На данный момент сайт был довольно надежным для меня. Я подниму вопрос с ними. РЕДАКТИРОВАТЬ: я подтвердил, что сгенерированный код не компилируется; это не было ясно без этого редактирования.
между

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