Устранение неоднозначной перегрузки указателя функции и std :: function для лямбда с помощью +

94

В следующем коде первый вызов fooнеоднозначен и поэтому не может быть скомпилирован.

Второй, с добавленным +перед лямбдой, разрешает перегрузку указателя функции.

#include <functional>

void foo(std::function<void()> f) { f(); }
void foo(void (*f)()) { f(); }

int main ()
{
    foo(  [](){} ); // ambiguous
    foo( +[](){} ); // not ambiguous (calls the function pointer overload)
}

Что +здесь делают обозначения?

Стив Лоример
источник

Ответы:

100

В +выражении +[](){}есть унарный +оператор. В [expr.unary.op] / 7 это определено следующим образом:

Операнд унарного +оператора должен иметь арифметический, перечисление без области видимости или тип указателя, а результатом является значение аргумента.

Лямбда не имеет арифметического типа и т. Д., Но может быть преобразована:

[expr.prim.lambda] / 3

Тип лямбда-выражения [...] является уникальным, безымянным типом класса без объединения, называемым типом замыкания , свойства которого описаны ниже.

[expr.prim.lambda] / 6

Тип закрытия для лямбда-выражения , без лямбда-захвата имеет publicне- virtualне- explicit constфункцию преобразования в указатель на функцию , имеющую те же значения параметров и возвращаемых типов в качестве оператора вызова функция замыкающим в. Значение, возвращаемое этой функцией преобразования, должно быть адресом функции, которая при вызове имеет тот же эффект, что и вызов оператора вызова функции закрывающего типа.

Следовательно, унарный принудительно +приводит к преобразованию в тип указателя функции, который предназначен для этой лямбды void (*)(). Следовательно, тип выражения +[](){}- это тип указателя на функцию void (*)().

Вторая перегрузка void foo(void (*f)())становится точным совпадением в рейтинге для разрешения перегрузки и поэтому выбирается однозначно (поскольку первая перегрузка НЕ ​​является точным совпадением).


Лямбда [](){}может быть преобразован в std::function<void()>через без явного шаблона CTOR из std::function, который принимает любой тип , который удовлетворяет Callableи CopyConstructibleтребованиям.

Лямбда также может быть преобразована void (*)()в функцию преобразования типа закрытия (см. Выше).

Оба являются пользовательскими последовательностями преобразования и имеют одинаковый ранг. Вот почему в первом примере разрешение перегрузки не удается из-за неоднозначности.


По словам Кассио Нери, подкрепленного аргументом Даниэля Крюглера, этот унарный +трюк должен иметь конкретное поведение, то есть на него можно положиться (см. Обсуждение в комментариях).

Тем не менее, я бы рекомендовал использовать явное приведение к типу указателя функции, если вы хотите избежать двусмысленности: вам не нужно спрашивать у SO, что он делает и почему он работает;)

дип
источник
3
Указатели на функции-члены @Fred AFAIK нельзя преобразовать в указатели на функции, не являющиеся членами, не говоря уже о lvalue функций. Вы можете привязать функцию-член через std::bindк std::functionобъекту, который может вызываться аналогично функции lvalue.
dyp
2
@DyP: Я считаю, что мы можем положиться на хитрости. В самом деле, предположим, что реализация добавляет operator +()к типу закрытия без сохранения состояния. Предположим, что этот оператор возвращает нечто иное, чем указатель на функцию, в которую преобразуется тип замыкания. Тогда это изменит наблюдаемое поведение программы, нарушающей 5.1.2 / 3. Пожалуйста, дайте мне знать, согласны ли вы с этим рассуждением.
Cassio Neri
2
@CassioNeri Да, я не уверен в этом. Я согласен с тем, что наблюдаемое поведение может измениться при добавлении operator +, но это по сравнению с ситуацией, которой нет operator +для начала. Но не указано, что тип замыкания не должен иметь operator +перегрузки. «Реализация может определять тип закрытия иначе, чем описано ниже, при условии, что это не изменяет наблюдаемое поведение программы, кроме как на [...]», но добавление оператора IMO не изменяет тип закрытия на что-то отличное от того, что "описано ниже".
dyp
3
@DyP: Ситуация, когда нет operator +(), точно описана стандартом. Стандарт позволяет реализации делать что-то отличное от указанного. Например, добавление operator +(). Однако, если это различие наблюдается программой, то это незаконно. Однажды я спросил на comp.lang.c ++. Moderated, может ли тип закрытия добавить typedef для result_typeи другой, typedefsнеобходимый для их адаптации (например, by std::not1). Мне сказали, что не может, потому что это можно было наблюдать. Попробую найти ссылку.
Cassio Neri
6
VS15 выдает забавную ошибку: test.cpp (543): error C2593: 'operator +' неоднозначен t \ test.cpp (543): note: может быть 'встроенный C ++ operator + (void (__cdecl *) (void )) 't \ test.cpp (543): note: or' встроенный оператор C ++ + (void (__stdcall *) (void)) 't \ test.cpp (543): note: or' встроенный оператор C ++ + (void (__fastcall *) (void)) 't \ test.cpp (543): note: или' встроенный оператор C ++ + (void (__vectorcall *) (void)) 't \ test.cpp (543): note : при попытке сопоставить список аргументов '(wmain :: <lambda_d983319760d11be517b3d48b95b3fe58>) test.cpp (543): ошибка C2088:' + ': недопустимо для класса
Эд Ламберт