Почему переменная const иногда не требуется для захвата в лямбда?

Рассмотрим следующий пример:

#include <cstdlib>

int main() {
    const int m = 42;
    [] { m; }(); // OK

    const int n = std::rand();
    [] { n; }(); // error: 'n' is not captured
}

Почему мне нужно захватить n во второй лямбда, но не m в первой лямбда? Я проверил раздел 5.1.2 (Лямбда-выражения) в стандарте С++ 14, но мне не удалось найти причину. Можете ли вы указать мне абзац, в котором это объясняется?

Обновление: я наблюдал это поведение как с GCC 6.3.1, так и с 7 (trunk). Clang 4.0 и 5 (trunk) завершаются с ошибкой в ​​обоих случаях (variable 'm' cannot be implicitly captured in a lambda with no capture-default specified).

Ответ 1

Для лямбды в области блока переменные, отвечающие определенным критериям в области охвата, могут использоваться ограниченным образом внутри лямбда, даже если они не захвачены.

Грубо говоря, область охвата включает любую переменную, локальную для функции, содержащую лямбда, которая была бы в области видимости в точке, определяемой лямбдой. Таким образом, это включает m и n в приведенных выше примерах.

"Определенные критерии" и "ограниченные способы" конкретно (как на С++ 14):

  • Внутри лямбда переменная не должна использоваться как odr, что означает, что она не должна подвергаться какой-либо операции, за исключением:
    • представляющий собой выражение с отброшенным значением (m; является одним из них) или
    • получив свое значение.
  • Переменная должна быть:
    • A const, non volatile integer или enum, инициатор которого был константным выражением, или
    • A constexpr, non volatile variable (или подобъект такого)

Ссылки на С++ 14: [expr.const]/2.7, [basic.def.odr]/3 (первое предложение), [expr.prim.lambda]/12, [expr.prim.lambda]/10.

Обоснование этих правил, как это было предложено другими комментариями/ответами, заключается в том, что компилятор должен иметь возможность "синтезировать" лямбда без захвата как свободную функцию, независимую от блока (поскольку такие вещи могут быть преобразованы в указатель на функцию); он может это сделать, несмотря на обращение к переменной, если он знает, что переменная всегда будет иметь одно и то же значение или она может повторить процедуру получения значения переменной, не зависящей от контекста. Но это не может быть сделано, если переменная может время от времени отличаться или если требуется, например, адрес переменной.


В вашем коде n был инициализирован не константным выражением. Поэтому n нельзя использовать в лямбда без захвата.

m был инициализирован константным выражением 42, поэтому он соответствует "определенным критериям". Выражение отбрасываемого значения не использует выражение odr, поэтому m; можно использовать без m. gcc правильно.


Я бы сказал, что разница между двумя компиляторами заключается в том, что clang рассматривает m; как odr-use m, но gcc этого не делает. Первое предложение [basic.def.odr]/3 довольно сложно:

Переменная x, имя которой отображается как потенциально оцениваемое выражение ex, является odr-используемым ex, если применение преобразования lvalue-to-rval в x не дает выражения константы, которое не вызывает никаких нетривиальные функции, и если x является объектом, ex является элементом набора потенциальных результатов выражения e, где либо преобразование lvalue-to-rale применяется к e, либо e - выражение с отбрасыванием.

но при чтении внимательно упоминает, что выражение с отброшенным значением не использует odr-выражение.

Версия С++ 11 [basic.def.odr] изначально не включала случай выражения сбрасываемым значением, поэтому поведение clang было бы правильным в опубликованном С++ 11. Однако текст, который появляется в С++ 14, был принят как Дефект против С++ 11 (Проблема 712), поэтому компиляторы должны обновлять свое поведение даже в режиме С++ 11.

Ответ 2

Потому что это постоянное выражение, компилятор рассматривает как будто он [] { 42; }();

Правило в [expr.prim.lambda]:

Если лямбда-выражение или инстанцирование вызова функции шаблон оператора общего лямбда-odr-uses (3.2) это или переменная с автоматическим временем хранения от ее охвата, этот объект должен быть захвачен лямбда-выражением.

Здесь цитата из стандарта [basic.def.odr]:

Переменная x, имя которой появляется как потенциально-оцененное выражение ex, используемое odr, если только применение преобразования lvalue-to-rval в x дает постоянное выражение (...) или e - выражение с отброшенными значениями.

(Удалена не так важная часть, чтобы держать ее коротким)

Мое простое понимание: компилятор знает, что m является постоянным во время компиляции, тогда как n будет меняться во время выполнения и, следовательно, n должен быть захвачен. n будет использоваться как odr, потому что вы должны действительно взглянуть на то, что находится внутри n во время выполнения. Другими словами, имеет значение тот факт, что "существует только одно" определение n.

Это из комментария M.M:

m является константным выражением, поскольку оно является автоматической переменной const с константным инициализатором выражения, но n не является константой потому что его инициализатор не был постоянным выражением. Эта рассматривается в [expr.const]/2.7. Постоянное выражение не ODR, в соответствии с первым предложением [basic.def.odr]/3

Смотрите здесь демо.

Ответ 3

EDIT: предыдущая версия моего ответа была неправильной. Начинающий правильный, вот соответствующая стандартная цитата:

[basic.def.odr]

  1. Переменная x, имя которой отображается как потенциально оцененное выражение ex, odr-used ex, если не применить lvalue- to-rvalue преобразование в x дает константное выражение, которое не вызывает никаких нетривиальных функций и, если x - объект, ex - элемент множества потенциальных результатов выражения e, где либо преобразование lvalue-rval применяется к e, либо e - выражение с отброшенным значением....

Так как m является постоянным выражением, оно не используется odr и, следовательно, его не нужно захватывать.

Похоже, что поведение clangs не соответствует стандарту.