Из cppinsights мы видим, как в следующем коде строка lambda();
интерпретируется языком:
const auto lambda = [] () mutable {};
void foo()
{
lambda();
}
Можно было бы наивно подумать, что эта строка вызывает лямбду (неконстантную) operator()
, которая, в свою очередь, не компилируется. Но вместо этого компилятор преобразует нефиксирующую лямбду в указатель функции, вызывает указатель функции и принимает код.
Какова цель этого преобразования? На мой взгляд, было бы более логично, если бы это было отклонено. Нет никаких признаков того, что программист намеревался выполнить это преобразование. Язык делает это сам.
Обратите внимание, что преобразование происходит только в приведенном выше случае, когда вызов lambda.operator()()
отбрасывает квалификаторы. Этого не происходит (т. е. operator()
вызывается напрямую), если lambda
не является const
или operator()
не отмечен mutable
.
Вы можете видеть это, когда отметка лямбда-функции mutable
делает каждую переменную-член mutable
, в то время как lambda
по-прежнему остается const
. Я думаю, это могло бы сделать operator()
const
квалифицированным, а затем просто посыпать mutable
все переменные-члены вместо того, чтобы смывать const
с помощью функции static
, которая вызывает не const
квалифицированный operator()
, но, вероятно, в этом есть обратная сторона - или ее нет, и некоторые вместо этого это делает другая реализация.
О, кажется, я только что осознал обратную сторону получения operator()
const
квалификации. Квалификаторы в настоящее время (должны быть) частью сигнатуры функции, если я не ошибаюсь.
@FrançoisAndrieux: Действительно. Обычные занятия работают так же. Демо.
«Этого не произойдет [...], если lambda
не является const
или operator()
не отмечено mutable
». -- Или если lambda
фиксирует что-то (что по совпадению включает в себя случай, когда mutable
предназначен для воздействия).
Объявление лямбды как 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 user17732522 Я обновил свой ответ, включив в него разрешение перегрузки.
Пусть Lambda
— тип лямбда-замыкания. Таким образом, decltype(lambda)
есть const Lambda
.
Выражение lambda()
можно интерпретировать по-разному. Это может быть – в данном случае –
Lambda::operator()
, илиlambda
. Поскольку список захвата лямбда-выражения пуст, существует функция преобразования в void (*)()
(см. правило ). Мы также можем видеть это в cppinsights.Это подробно описано в разрешении перегрузки:
Если
E
в выражении вызова функцииE(args)
имеет тип класса cvT
, то
Операторы вызова функции
T
получаются путем обычного поиска имениoperator()
в контексте выражения(E).operator()
, и каждое найденное объявление добавляется к набору функций-кандидатов.Для каждой неявной пользовательской функции преобразования в
T
или в базеT
(если она не скрыта), чьи cv-квалификаторы совпадают или превышают cv-квалификаторыT
, и где функция преобразования преобразуется в:
- указатель на функцию
- ссылка на указатель на функцию
- ссылка на функцию
затем суррогатную функцию вызова с уникальным именем, первый параметр которой является результатом преобразования, остальные параметры — это список параметров, принятый результатом преобразования, а тип возвращаемого значения — это возвращаемый тип результата преобразования, добавляется в набор функций-кандидатов. Если эта суррогатная функция выбирается при последующем разрешении перегрузки, то будет вызвана определяемая пользователем функция преобразования, а затем будет вызван результат преобразования.
В этом случае в E(args)
E
есть lambda
, args
пусто, а cv T
— это const Lambda
, тип класса. На это выражение в конкурсе участвуют два кандидата:
void Lambda::operator()()
, неконстантный оператор вызова функции лямбда-замыкания, иvoid SurrogateFunction(void(*)())
, изобретенная суррогатная функция.Перепишем первого кандидата в виде глобальной функции, чтобы было проще понять. В этом случае его первым параметром будет объект *this
:
void FunctionCallOperator(Lambda&)
,void SurrogateFunction(void(*)())
.В этих двух тестах используется выражение lvalue lambda
типа const Lambda
.
Lambda
.Итак, за происходящим нет никакой скрытой цели. Функция преобразования выигрывает при разрешении перегрузки, что является общим правилом, не специфичным для лямбда-выражений.
Вероятно, это результат обычного разрешения перегрузки, а не конструктивного решения. Последовательность преобразования допустима, но менее предпочтительна, чем называть ее
operator()
, которую она обычно использует. Но поскольку предпочтительное решение неприменимо, оно просто превращается в менее желательное, но законное.