Захват ссылки по ссылке в С++ 11 лямбда
Рассмотрим это:
#include <functional>
#include <iostream>
std::function<void()> make_function(int& x) {
return [&]{ std::cout << x << std::endl; };
}
int main() {
int i = 3;
auto f = make_function(i);
i = 5;
f();
}
Гарантируется ли этой программе вывод 5
без вызова поведения undefined?
Я понимаю, как это работает, если я фиксирую x
по значению ([=]
), но я не уверен, вызываю ли я поведение undefined путем его захвата по ссылке. Может быть, я вернусь к обвисшей ссылке после того, как make_function
вернется, или гарантированное выполнение захваченного задания будет продолжаться до тех пор, пока объект, на который ссылается исходный объект, все еще существует?
Ищем окончательные ответы на основе стандартов здесь:) Это работает достаточно хорошо на практике до сих пор;)
Ответы
Ответ 1
Код гарантированно работает.
Прежде чем углубиться в стандартную формулировку: комитет С++ намерен использовать этот код. Тем не менее, формулировка в ее нынешнем виде считалась недостаточно понятной для этого (и действительно, исправления, сделанные для стандартного пост-С++ 14, нарушили тонкую схему, которая заставила его работать), поэтому Вопрос CWG 2011 был поднят для выяснения вопросов и сейчас проходит через комитет. Насколько мне известно, реализация не ошибается.
Я хотел бы прояснить пару вещей, потому что ответ Ben Voigt содержит некоторые фактические ошибки, которые создают некоторую путаницу:
- "Сфера" - это статическое лексическое понятие в С++, которое описывает регион исходного кода программы, в котором неквалифицированный поиск имени связывает конкретное имя с объявлением. Это не имеет никакого отношения к жизни. См. [basic.scope.declarative]/1.
-
Правила "достижения области" для лямбда также являются синтаксическим свойством, которое определяет, когда разрешено захват. Например:
void f(int n) {
struct A {
void g() { // reaching scope of lambda starts here
[&] { int k = n; };
// ...
n
находится здесь в области видимости, но область охвата лямбда не включает его, поэтому он не может быть захвачен. Иными словами, достижимая область лямбда заключается в том, насколько "вверх" она может достигать и захватывать переменные - она может достигать закрывающей (не-лямбда) функции и ее параметров, но она не может выйти за ее пределы захватить объявления, которые появляются снаружи.
Таким образом, понятие "охват" не имеет отношения к этому вопросу. Объект, захваченный, является make_function
параметром x
, который находится в пределах области охвата лямбда.
ОК, так что давайте рассмотрим стандартную формулировку по этому вопросу. Per [expr.prim.lambda]/17, только id-выражения, относящиеся к объектам, захваченным копией, преобразуются в членский доступ к типу закрытия лямбда; id-выражения, относящиеся к объектам, захваченным ссылкой, остаются в силе и по-прежнему обозначают тот же объект, который они обозначили бы в охватывающей области.
Это сразу кажется плохим: конец ссылки x
закончился, поэтому как мы можем это назвать? Ну, оказывается, что почти нет (см. Ниже) никакого способа ссылаться на ссылку за пределами ее жизни (вы можете либо увидеть ее декларацию, в этом случае она в области видимости и, следовательно, предположительно ОК для использования, либо это класс член, и в этом случае сам класс должен быть в пределах своего жизненного цикла, чтобы выражение доступа к члену было действительным). В результате стандарт не имел никаких запретов на использование ссылки за пределами своей жизни до недавнего времени.
Лямбда-формулировка воспользовалась тем, что нет штрафа за использование ссылки за пределами ее срока службы, и поэтому не нужно было указывать какие-либо четкие правила для того, какой доступ к объекту, захваченному ссылочным средством, - это просто означает вы используете этот объект; если это ссылка, имя обозначает его инициализатор. И то, как это гарантировалось, будет работать до самого недавнего времени (в том числе на С++ 11 и С++ 14).
Однако, не совсем верно, что вы не можете упоминать ссылку за пределами ее жизни; в частности, вы можете ссылаться на него из своего собственного инициализатора, начиная с инициализатора члена класса раньше ссылочной или если это переменная области пространства имен и вы получаете доступ к ней из другого глобального, который инициализирован до его появления. была выпущена проблема CWG 2012 для исправления этого недосмотра, но она непреднамеренно нарушила спецификацию для лямбда-захвата по ссылке. Мы должны получить эту регрессию до того, как корабли С++ 17; Я подал комментарий Национального органа, чтобы убедиться, что он имеет приоритет.
Ответ 2
TL; DR: код в вопросе не гарантируется Стандартом, и есть разумные реализации лямбда, которые заставляют его сломаться. Предположим, что он не переносится и вместо этого использует
std::function<void()> make_function(int& x)
{
const auto px = &x;
return [/* = */ px]{ std::cout << *px << std::endl; };
}
Начиная с С++ 14, вы можете отказаться от явного использования указателя с использованием инициализированного захвата, который заставляет новую ссылочную переменную быть создан для лямбда, вместо повторного использования той, которая находится в охватывающей области:
std::function<void()> make_function(int& x)
{
return [&x = x]{ std::cout << x << std::endl; };
}
На первый взгляд кажется, что это должно быть безопасно, но формулировка стандарта вызывает немного проблемы:
Лямбда-выражение, наименьшей охватывающей областью которого является область блока (3.3.3), является локальным лямбда-выражением; любое другое лямбда-выражение не должно иметь захват-дефолт или простой захват в его лямбда-интродукторе. Область охвата локального лямбда-выражения представляет собой набор охватывающих областей до и включая внутренняя закрывающая функция и ее параметры.
...
Все такие неявно захваченные объекты должны быть объявлены в пределах охвата лямбда-выражения.
...
[Примечание. Если объект неявно или явно захвачен ссылкой, вызов оператора вызова функции соответствующего лямбда-выражения после окончания срока службы объекта может привести к поведению undefined. - end note]
Мы ожидаем, что x
, как используется внутри make_function
, относится к i
в main()
(так как это то, что делают ссылки), а объект i
записывается по ссылке. Поскольку этот объект все еще живет во время вызова лямбды, все хорошо.
Но! "неявно захваченные объекты" должны быть "в пределах охвата лямбда-выражения", а i
в main()
не находится в области охвата.:( Если параметр x
не считается "объявленным в пределах области охвата", даже если сам объект i
находится за пределами области охвата.
Похоже, что , в отличие от любого другого места на С++, создается ссылка на ссылку, а время жизни ссылки имеет смысл.
Определенно, что-то, что я хотел бы, чтобы Стандарт уточнил.
Тем временем вариант, показанный в разделе TL; DR, определенно безопасен, поскольку указатель захватывается значением (хранится внутри самого объекта лямбда), и он является действительным указателем на объект, который длится через вызов лямбда. Я также ожидал бы, что захват по ссылке фактически заканчивается тем, что сохраняет указатель в любом случае, поэтому для этого не должно быть штрафа за выполнение.
При ближайшем рассмотрении мы также представляем, что он может сломаться. Помните, что на x86 в конечном машинный код доступны как локальные переменные, так и функциональные параметры, используя относительную адресацию EBP. Параметры имеют положительное смещение, а локальные - отрицательные. (Другие архитектуры имеют разные имена регистров, но многие работают одинаково). В любом случае это означает, что привязка по ссылке может быть реализована путем захвата только значения EBP. Затем локали и параметры могут снова быть найдены через относительную адресацию. И на самом деле я считаю, что я слышал о реализациях лямбда (на языках, которые были lambdas задолго до С++), делая именно это: захват "фрейма стека", где была определена лямбда.
Что подразумевается в том, что когда make_function
возвращается, и его стек стека уходит, так же как и всякая возможность доступа к параметрам locals AND, даже к тем, которые являются ссылками.
И стандарт содержит следующее правило, скорее всего, для включения этого подхода:
Не указано, объявлены ли в типе закрытия объявленные дополнительные элементы нестатического элемента для объектов, захваченных ссылкой.
Заключение: код в вопросе не гарантируется Стандартом, и есть разумные реализации лямбда, которые заставляют его сломаться. Предположим, что он не переносится.