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