Захват ссылки по ссылке в С++ 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, даже к тем, которые являются ссылками.

И стандарт содержит следующее правило, скорее всего, для включения этого подхода:

Не указано, объявлены ли в типе закрытия объявленные дополнительные элементы нестатического элемента для объектов, захваченных ссылкой.

Заключение: код в вопросе не гарантируется Стандартом, и есть разумные реализации лямбда, которые заставляют его сломаться. Предположим, что он не переносится.