Генерация лямбда-кода на C ++ с помощью Init Capture на C ++ 14 - PullRequest
10 голосов
/ 08 октября 2019

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

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

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
};

Как будет выглядеть конструктор? Это также перемещает это в член?

Ответы [ 3 ]

10 голосов
/ 08 октября 2019

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

5 голосов
/ 15 октября 2019

Нет необходимости спекулировать, используя 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.

2 голосов
/ 17 октября 2019

На этот вопрос нельзя ответить полностью в коде. Возможно, вам удастся написать несколько «эквивалентный» код, но стандарт не указывается таким образом.

С этим из пути, давайте углубимся в [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-выражение в составном операторе лямбда-выражения , который представляет собой использование odr объекта, захваченного копией, преобразуется в доступ к соответствующему безымянному элементу данных типа замыкания. [...]

[expr.prim.lambda.capture]/15

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

Давайте применим это к вашему случаю 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: выполняется только минимальное количество копий / инициализаций / ходов (как было бынадежда / ожидание). Я бы предположил, что лямбды не определены в терминах преобразования источника (в отличие от другого синтаксического сахара) точно , потому что выражение вещей в терминах конструкторов потребовало быЛишние операции.

Я надеюсь, что это уладит страхи, выраженные в вопросе:)

...