Menyelesaikan overload ambigu pada function pointer dan std :: function untuk lambda menggunakan +

94

Dalam kode berikut, panggilan pertama ke fooambigu, dan karena itu gagal untuk dikompilasi.

Yang kedua, dengan ditambahkan +sebelum lambda, menyelesaikan overload pointer fungsi.

#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)
}

Apa yang +dilakukan notasi di sini?

Steve Lorimer
sumber

Jawaban:

100

Dalam +ekspresi tersebut +[](){}adalah +operator unary . Ini didefinisikan sebagai berikut dalam [expr.unary.op] / 7:

Operand dari +operator unary harus memiliki aritmatika, enumerasi tanpa batas, atau tipe pointer dan hasilnya adalah nilai argumen.

Lambda bukanlah tipe aritmatika dll., Tetapi dapat dikonversi:

[expr.prim.lambda] / 3

Tipe ekspresi lambda [...] adalah tipe kelas non-union unik dan tidak bernama - disebut tipe closure - yang propertinya dijelaskan di bawah ini.

[expr.prim.lambda] / 6

Jenis penutupan untuk lambda ekspresi tanpa lambda-capture memiliki publicnon virtualnon explicit constfungsi konversi untuk pointer ke fungsi memiliki parameter dan return yang sama jenis sebagai operator fungsi panggilan jenis penutupan ini. Nilai yang dikembalikan oleh fungsi konversi ini harus menjadi alamat fungsi yang, ketika dipanggil, memiliki efek yang sama seperti memanggil operator panggilan fungsi tipe penutupan.

Oleh karena itu, unary +memaksa konversi ke tipe penunjuk fungsi, yang untuk lambda ini void (*)(). Oleh karena itu, tipe ekspresi +[](){}adalah tipe penunjuk fungsi ini void (*)().

Kelebihan muatan kedua void foo(void (*f)())menjadi Pencocokan Persis dalam peringkat untuk resolusi kelebihan muatan dan oleh karena itu dipilih dengan jelas (karena muatan berlebih pertama BUKAN Pencocokan Persis).


Lambda [](){}dapat dikonversi ke std::function<void()>melalui ctor template non-eksplisit std::function, yang mengambil jenis apa pun yang memenuhi persyaratan Callabledan CopyConstructible.

Lambda juga dapat dikonversi void (*)()melalui fungsi konversi tipe penutupan (lihat di atas).

Keduanya adalah urutan konversi yang ditentukan pengguna, dan memiliki peringkat yang sama. Itulah mengapa resolusi kelebihan beban gagal pada contoh pertama karena ambiguitas.


Menurut Cassio Neri, didukung oleh argumen Daniel Krügler, +trik unary ini harus ditentukan perilaku, yaitu Anda dapat mengandalkannya (lihat diskusi di komentar).

Namun, saya akan merekomendasikan menggunakan cast eksplisit ke jenis penunjuk fungsi jika Anda ingin menghindari ambiguitas: Anda tidak perlu bertanya pada SO apa itu dan mengapa berfungsi;)

dyp
sumber
3
Pointer fungsi anggota @Fred AFAIK tidak dapat diubah menjadi penunjuk fungsi non-anggota, apalagi nilai l fungsi. Anda dapat mengikat fungsi anggota melalui std::bindke std::functionobjek yang bisa disebut mirip dengan nilai fungsi.
dyp
2
@DyP: Saya yakin kita bisa mengandalkan trik. Memang, anggaplah sebuah implementasi menambah operator +()tipe penutupan tanpa kewarganegaraan. Asumsikan bahwa operator ini mengembalikan sesuatu selain penunjuk ke fungsi yang dikonversi menjadi tipe penutupan. Kemudian, ini akan mengubah perilaku program yang dapat diamati yang melanggar 5.1.2 / 3. Tolong beri tahu saya jika Anda setuju dengan alasan ini.
Cassio Neri
2
@CassioNeri Ya, di situlah saya tidak yakin. Saya setuju perilaku yang dapat diamati dapat berubah ketika menambahkan operator +, tetapi ini membandingkan dengan situasi yang operator +awalnya tidak ada . Namun tidak ditentukan bahwa tipe penutupan tidak boleh operator +kelebihan beban. "Implementasi dapat mendefinisikan jenis penutupan secara berbeda dari apa yang dijelaskan di bawah asalkan ini tidak mengubah perilaku program yang dapat diamati selain oleh [...]" tetapi IMO menambahkan operator tidak mengubah jenis penutupan menjadi sesuatu yang berbeda dari apa adalah "dijelaskan di bawah".
dyp
3
@DyP: Situasi di mana tidak ada operator +()sama persis dengan yang dijelaskan oleh standar. Standar memungkinkan implementasi untuk melakukan sesuatu yang berbeda dari apa yang ditentukan. Misalnya, menambahkan operator +(). Namun, jika perbedaan ini dapat diamati oleh suatu program, maka itu ilegal. Suatu kali saya bertanya di comp.lang.c ++. Dimoderasi apakah tipe closure bisa menambahkan typedef untuk result_typedan yang lain typedefsdiperlukan untuk membuatnya bisa beradaptasi (misalnya oleh std::not1). Saya diberitahu bahwa itu tidak bisa karena ini dapat diamati. Saya akan mencoba mencari tautannya.
Cassio Neri
6
VS15 memberi Anda kesalahan menyenangkan ini: test.cpp (543): kesalahan C2593: 'operator +' ambigu t \ test.cpp (543): note: bisa jadi 'built-in C ++ operator + (void (__cdecl *) (void )) 't \ test.cpp (543): note: atau' built-in C ++ operator + (void (__stdcall *) (void)) 't \ test.cpp (543): note: atau' built-in C ++ operator + (void (__fastcall *) (void)) 't \ test.cpp (543): note: atau' built-in C ++ operator + (void (__vectorcall *) (void)) 't \ test.cpp (543): note : ketika mencoba untuk mencocokkan daftar argumen '(wmain :: <lambda_d983319760d11be517b3d48b95b3fe58>) test.cpp (543): kesalahan C2088:' + ': ilegal untuk kelas
Ed Lambert