Почему изменяемая лямбда преобразуется в указатель на функцию вместо вызова оператора()?

Из cppinsights мы видим, как в следующем коде строка lambda(); интерпретируется языком:

const auto lambda = [] () mutable {};

void foo()
{
    lambda();
}

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

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

Обратите внимание, что преобразование происходит только в приведенном выше случае, когда вызов lambda.operator()() отбрасывает квалификаторы. Этого не происходит (т. е. operator() вызывается напрямую), если lambda не является const или operator() не отмечен mutable.

Вероятно, это результат обычного разрешения перегрузки, а не конструктивного решения. Последовательность преобразования допустима, но менее предпочтительна, чем называть ее operator(), которую она обычно использует. Но поскольку предпочтительное решение неприменимо, оно просто превращается в менее желательное, но законное.

François Andrieux 15.05.2024 22:07

Вы можете видеть это, когда отметка лямбда-функции mutable делает каждую переменную-член mutable, в то время как lambda по-прежнему остается const. Я думаю, это могло бы сделать operator()const квалифицированным, а затем просто посыпать mutable все переменные-члены вместо того, чтобы смывать const с помощью функции static, которая вызывает не const квалифицированный operator(), но, вероятно, в этом есть обратная сторона - или ее нет, и некоторые вместо этого это делает другая реализация.

Ted Lyngmo 15.05.2024 22:07

О, кажется, я только что осознал обратную сторону получения operator()const квалификации. Квалификаторы в настоящее время (должны быть) частью сигнатуры функции, если я не ошибаюсь.

Ted Lyngmo 15.05.2024 22:14

@FrançoisAndrieux: Действительно. Обычные занятия работают так же. Демо.

Dr. Gut 15.05.2024 22:48

«Этого не произойдет [...], если lambda не является const или operator() не отмечено mutable». -- Или если lambda фиксирует что-то (что по совпадению включает в себя случай, когда mutable предназначен для воздействия).

JaMiT 16.05.2024 03:47
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
5
140
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Объявление лямбды как mutable означает, что ее operator() может изменять захваченные переменные, а это означает, что operator() не может быть const. Но lambda — это объект const, поэтому к нему нельзя вызвать неконстантный operator() (и действительно, если вы попытаетесь, компилятор сообщит об ошибке).

Однако разрешение перегрузки принимает тип класса, который имеет оператор преобразования в указатель на функцию:

Вызов объекта класса

Если E в выражении вызова функции E(args) имеет тип класса cv T, то

  • Операторы вызова функции T получаются путем обычного поиска имени operator() в контексте выражения (E).operator(), и каждое найденное объявление добавляется к набору функций-кандидатов.

  • Для каждой неявной пользовательской функции преобразования в T или в базе T (если она не скрыта), чьи cv-квалификаторы совпадают или превышают cv-квалификаторы T, и где функция преобразования преобразуется в:

    • указатель на функцию
    • ссылка на указатель на функцию
    • ссылка на функцию

    затем суррогатную функцию вызова с уникальным именем, первый параметр которой является результатом преобразования, остальные параметры — это список параметров, принятый результатом преобразования, а тип возвращаемого значения — это возвращаемый тип результата преобразования, добавляется в набор функций-кандидатов. Если эта суррогатная функция выбирается при последующем разрешении перегрузки, то будет вызвана определяемая пользователем функция преобразования, а затем будет вызван результат преобразования.

А поскольку лямбда — это тип класса, а лямбда без захвата неявно преобразуется в указатель на функцию, компилятор преобразует объект lambda в указатель на функцию, а затем вызывает функцию.

Формально раздел, который вы цитируете из cppreference, не применяется, потому что он применяется только в том случае, если function имеет тип указателя на функцию или функцию. Он не допускает какой-либо неявной последовательности преобразования. Вместо этого поведение указывается как особый случай при разрешении перегрузки, см. «функцию суррогатного вызова» в en.cppreference.com/w/cpp/language/overload_solve.

user17732522 16.05.2024 07:40

@user17732522 user17732522 Я обновил свой ответ, включив в него разрешение перегрузки.

Remy Lebeau 16.05.2024 16:58
Ответ принят как подходящий

Пусть Lambda — тип лямбда-замыкания. Таким образом, decltype(lambda) есть const Lambda.

Выражение lambda() можно интерпретировать по-разному. Это может быть – в данном случае –

  • звонок на Lambda::operator(), или
  • вызов указателя на функцию, полученный в результате преобразования lambda. Поскольку список захвата лямбда-выражения пуст, существует функция преобразования в void (*)() (см. правило ). Мы также можем видеть это в cppinsights.

Это подробно описано в разрешении перегрузки:

Если E в выражении вызова функции E(args) имеет тип класса cv T, то

  • Операторы вызова функции T получаются путем обычного поиска имени operator() в контексте выражения (E).operator(), и каждое найденное объявление добавляется к набору функций-кандидатов.

  • Для каждой неявной пользовательской функции преобразования в T или в базе T (если она не скрыта), чьи cv-квалификаторы совпадают или превышают cv-квалификаторы T, и где функция преобразования преобразуется в:

    • указатель на функцию
    • ссылка на указатель на функцию
    • ссылка на функцию

    затем суррогатную функцию вызова с уникальным именем, первый параметр которой является результатом преобразования, остальные параметры — это список параметров, принятый результатом преобразования, а тип возвращаемого значения — это возвращаемый тип результата преобразования, добавляется в набор функций-кандидатов. Если эта суррогатная функция выбирается при последующем разрешении перегрузки, то будет вызвана определяемая пользователем функция преобразования, а затем будет вызван результат преобразования.

В этом случае в E(args)E есть lambda, args пусто, а cv T — это const Lambda, тип класса. На это выражение в конкурсе участвуют два кандидата:

  1. void Lambda::operator()(), неконстантный оператор вызова функции лямбда-замыкания, и
  2. void SurrogateFunction(void(*)()), изобретенная суррогатная функция.

Перепишем первого кандидата в виде глобальной функции, чтобы было проще понять. В этом случае его первым параметром будет объект *this:

  1. void FunctionCallOperator(Lambda&),
  2. void SurrogateFunction(void(*)()).

В этих двух тестах используется выражение lvalue lambda типа const Lambda.

  1. 1st нельзя назвать. Конверсия приведет к потере квалификаторов.
  2. 2nd можно вызвать с помощью определяемого пользователем преобразования Lambda.

Итак, за происходящим нет никакой скрытой цели. Функция преобразования выигрывает при разрешении перегрузки, что является общим правилом, не специфичным для лямбда-выражений.

Другие вопросы по теме