Mengapa merancang bahasa dengan tipe anonim yang unik?


90

Ini adalah sesuatu yang selalu mengganggu saya sebagai fitur ekspresi lambda C ++: Jenis ekspresi lambda C ++ unik dan anonim, saya tidak bisa menuliskannya. Bahkan jika saya membuat dua lambda yang secara sintaksis persis sama, tipe yang dihasilkan didefinisikan berbeda. Konsekuensinya adalah, a) lambda hanya dapat diteruskan ke fungsi templat yang memungkinkan waktu kompilasi, tipe yang tidak dapat disebutkan untuk diteruskan bersama dengan objek, dan b) lambda itu hanya berguna setelah tipe dihapus melalui std::function<>.

Oke, tapi begitulah C ++ melakukannya, saya siap untuk menghapusnya hanya sebagai fitur menjengkelkan dari bahasa itu. Namun, saya baru mengetahui bahwa Rust tampaknya melakukan hal yang sama: Setiap fungsi Rust atau lambda memiliki tipe anonim yang unik. Dan sekarang saya bertanya-tanya: Mengapa?

Jadi, pertanyaan saya adalah ini:
Apa keuntungan, dari sudut pandang desainer bahasa, untuk memperkenalkan konsep tipe anonim yang unik ke dalam bahasa?


6
seperti biasa, pertanyaan yang lebih baik adalah mengapa tidak.
Stargateur

31
"lambda itu hanya berguna setelah tipe mereka dihapus melalui std :: function <>" - tidak, mereka langsung berguna tanpa std::function. Lambda yang telah diteruskan ke fungsi template bisa dipanggil secara langsung tanpa melibatkan std::function. Kompilator kemudian dapat menyebariskan lambda ke dalam fungsi template yang akan meningkatkan efisiensi waktu proses.
Erlkoenig

1
Dugaan saya, penerapan lambda lebih mudah, dan bahasanya lebih mudah dipahami. Jika Anda mengizinkan ekspresi lambda yang sama persis untuk dilipat menjadi jenis yang sama, maka Anda memerlukan aturan khusus untuk ditangani { int i = 42; auto foo = [&i](){ return i; }; } { int i = 13; auto foo = [&i](){ return i; }; }karena variabel yang dirujuknya berbeda, meskipun secara tekstual keduanya sama. Jika Anda hanya mengatakan bahwa semuanya unik, Anda tidak perlu khawatir untuk mencoba mencari tahu.
NathanOliver

5
tetapi Anda juga dapat memberi nama untuk jenis lambda dan melakukan semua hal yang sama dengan itu. lambdas_type = decltype( my_lambda);
idclev 463035818

3
Tapi apa yang seharusnya menjadi jenis lambda generik [](auto) {}? Haruskah itu memiliki tipe, untuk memulai?
Evg

Jawaban:


78

Banyak standar (terutama C ++) mengambil pendekatan untuk meminimalkan berapa banyak yang mereka minta dari compiler. Terus terang, mereka sudah cukup menuntut! Jika mereka tidak harus menentukan sesuatu untuk membuatnya bekerja, mereka cenderung membiarkan implementasinya ditentukan.

Jika lambda tidak anonim, kami harus mendefinisikannya. Ini harus menjelaskan banyak hal tentang bagaimana variabel ditangkap. Pertimbangkan kasus lambda [=](){...}. Tipe harus menentukan tipe mana yang benar-benar ditangkap oleh lambda, yang bisa jadi tidak sepele untuk ditentukan. Juga, bagaimana jika kompilator berhasil mengoptimalkan variabel? Mempertimbangkan:

static const int i = 5;
auto f = [i]() { return i; }

Kompilator yang mengoptimalkan dapat dengan mudah mengenali bahwa satu-satunya nilai yang mungkin dari iyang dapat ditangkap adalah 5, dan menggantinya dengan auto f = []() { return 5; }. Namun, jika tipenya tidak anonim, ini bisa mengubah tipe atau memaksa compiler untuk lebih sedikit mengoptimalkan, menyimpan imeskipun sebenarnya tidak membutuhkannya. Ini adalah seluruh kantong kompleksitas dan nuansa yang tidak diperlukan untuk apa yang dimaksudkan oleh lambda.

Dan, jika Anda benar-benar membutuhkan tipe non-anonim, Anda selalu dapat membuat kelas closure sendiri, dan bekerja dengan functor daripada fungsi lambda. Dengan demikian, mereka dapat membuat lambda menangani kasus 99%, dan membiarkan Anda membuat kode solusi Anda sendiri dalam 1%.


Deduplicator menunjukkan dalam komentar bahwa saya tidak membahas keunikan sebanyak anonimitas. Saya kurang yakin tentang manfaat keunikan, tetapi perlu dicatat bahwa perilaku berikut ini jelas jika tipenya unik (tindakan akan dibuat dua kali).

int counter()
{
    static int count = 0;
    return count++;
}

template <typename FuncT>
void action(const FuncT& func)
{
    static int ct = counter();
    func(ct);
}

...
for (int i = 0; i < 5; i++)
    action([](int j) { std::cout << j << std::endl; });

for (int i = 0; i < 5; i++)
    action([](int j) { std::cout << j << std::endl; });

Jika jenisnya tidak unik, kita harus menentukan perilaku apa yang harus terjadi dalam kasus ini. Itu bisa jadi rumit. Beberapa isu yang diangkat pada topik anonimitas juga mengangkat kepala jelek mereka dalam hal ini keunikan.


Perhatikan bahwa ini bukan tentang menyimpan pekerjaan untuk pelaksana kompilator, tetapi menyimpan pekerjaan untuk pengelola standar. Kompilator masih harus menjawab semua pertanyaan di atas untuk implementasi spesifiknya, tetapi tidak ditentukan dalam standar.
ComicSansMS

2
@ComicSansMS Menyatukan hal-hal seperti itu saat mengimplementasikan kompiler jauh lebih mudah ketika Anda tidak harus menyesuaikan implementasi Anda dengan standar orang lain. Berbicara dari pengalaman, seringkali jauh lebih mudah bagi pengelola standar untuk menentukan fungsionalitas secara berlebihan daripada mencoba menemukan jumlah minimum untuk ditentukan sambil tetap mendapatkan fungsionalitas yang diinginkan dari bahasa Anda. Sebagai studi kasus yang sangat baik, lihat seberapa banyak pekerjaan yang mereka habiskan untuk menghindari spesifikasi memory_order_consume yang berlebihan sambil tetap membuatnya berguna (pada beberapa arsitektur)
Cort Ammon

1
Seperti orang lain, Anda membuat kasus yang menarik untuk anonim . Tetapi apakah benar-benar ide yang bagus untuk memaksanya menjadi unik juga?
Deduplicator

Bukan kompleksitas kompilator yang penting di sini, tetapi kompleksitas kode yang dihasilkan. Intinya bukanlah untuk membuat kompiler menjadi lebih sederhana, tetapi memberikan ruang gerak yang cukup untuk mengoptimalkan semua kasus dan menghasilkan kode natural untuk platform target.
Jan Hudec

Anda tidak dapat menangkap variabel statis.
Ruslan

70

Lambda bukan hanya fungsi, mereka adalah fungsi dan status . Oleh karena itu, C ++ dan Rust mengimplementasikannya sebagai objek dengan operator panggilan ( operator()dalam C ++, 3 Fn*ciri di Rust).

Pada dasarnya, [a] { return a + 1; }dalam C ++ mendeskripsikan sesuatu seperti

struct __SomeName {
    int a;

    int operator()() {
        return a + 1;
    }
};

kemudian menggunakan contoh di __SomeNamemana lambda digunakan.

Sedangkan di Rust, || a + 1di Rust akan desugar menjadi sesuatu seperti

{
    struct __SomeName {
        a: i32,
    }

    impl FnOnce<()> for __SomeName {
        type Output = i32;
        
        extern "rust-call" fn call_once(self, args: ()) -> Self::Output {
            self.a + 1
        }
    }

    // And FnMut and Fn when necessary

    __SomeName { a }
}

Artinya kebanyakan lambda pasti memiliki jenis yang berbeda .

Sekarang, ada beberapa cara untuk melakukannya:

  • Dengan tipe anonim, yang diterapkan oleh kedua bahasa. Konsekuensi lain dari itu adalah semua lambda harus memiliki jenis yang berbeda. Tetapi bagi perancang bahasa, ini memiliki keuntungan yang jelas: Lambdas dapat secara sederhana dijelaskan menggunakan bagian bahasa lain yang sudah ada yang lebih sederhana. Mereka hanyalah gula sintaks di sekitar bit bahasa yang sudah ada.
  • Dengan beberapa sintaks khusus untuk penamaan jenis lambda: Namun ini tidak diperlukan karena lambda sudah dapat digunakan dengan template di C ++ atau dengan generik dan Fn*ciri - ciri di Rust. Tidak ada bahasa yang memaksa Anda mengetik-hapus lambda untuk menggunakannya (dengan std::functionC ++ atau Box<Fn*>Rust).

Perhatikan juga bahwa kedua bahasa setuju bahwa lambda sepele yang tidak menangkap konteks dapat diubah menjadi penunjuk fungsi.


Mendeskripsikan fitur kompleks dari suatu bahasa menggunakan fitur yang lebih sederhana cukup umum. Misalnya, C ++ dan Rust memiliki loop range-for, dan keduanya mendeskripsikannya sebagai gula sintaks untuk fitur lainnya.

C ++ mendefinisikan

for (auto&& [first,second] : mymap) {
    // use first and second
}

sebagai setara dengan

{

    init-statement
    auto && __range = range_expression ;
    auto __begin = begin_expr ;
    auto __end = end_expr ;
    for ( ; __begin != __end; ++__begin) {

        range_declaration = *__begin;
        loop_statement

    }

} 

dan Rust mendefinisikan

for <pat> in <head> { <body> }

sebagai setara dengan

let result = match ::std::iter::IntoIterator::into_iter(<head>) {
    mut iter => {
        loop {
            let <pat> = match ::std::iter::Iterator::next(&mut iter) {
                ::std::option::Option::Some(val) => val,
                ::std::option::Option::None => break
            };
            SemiExpr(<body>);
        }
    }
};

yang meskipun tampak lebih rumit bagi manusia, keduanya lebih sederhana bagi perancang bahasa atau kompiler.


15
@ cmaster-reinstatemonica Pertimbangkan untuk meneruskan lambda sebagai argumen pembanding untuk fungsi penyortiran. Apakah Anda benar-benar ingin memaksakan overhead panggilan fungsi virtual di sini?
Daniel Langr

5
@ cmaster-reinstatemonica karena tidak ada yang virtual-secara-default di C ++
Caleth

4
@cmaster - Maksud Anda memaksa semua pengguna lambda untuk membayar dipatch dinamis, bahkan ketika mereka tidak membutuhkannya?
StoryTeller - Unslander Monica

4
@ cmaster-reinstatemonica Yang terbaik yang akan Anda dapatkan adalah ikut serta ke virtual. Coba tebak, std::functionapakah itu
Caleth

9
@ cmaster-reinstatemonica mekanisme apa pun di mana Anda dapat mengarahkan kembali fungsi yang akan dipanggil akan memiliki situasi dengan overhead waktu proses. Itu bukan cara C ++. Anda ikut serta denganstd::function
Caleth

13

(Menambah jawaban Caleth, tapi terlalu panjang untuk memuat komentar.)

Ekspresi lambda hanyalah gula sintaksis untuk struct anonim (tipe Voldemort, karena Anda tidak dapat menyebutkan namanya).

Anda dapat melihat kesamaan antara struct anonim dan anonimitas lambda dalam cuplikan kode ini:

#include <iostream>
#include <typeinfo>

using std::cout;

int main() {
    struct { int x; } foo{5};
    struct { int x; } bar{6};
    cout << foo.x << " " << bar.x << "\n";
    cout << typeid(foo).name() << "\n";
    cout << typeid(bar).name() << "\n";
    auto baz = [x = 7]() mutable -> int& { return x; };
    auto quux = [x = 8]() mutable -> int& { return x; };
    cout << baz() << " " << quux() << "\n";
    cout << typeid(baz).name() << "\n";
    cout << typeid(quux).name() << "\n";
}

Jika itu masih tidak memuaskan untuk lambda, seharusnya juga tidak memuaskan untuk struct anonim.

Beberapa bahasa memungkinkan jenis pengetikan bebek yang sedikit lebih fleksibel, dan meskipun C ++ memiliki templat yang tidak terlalu membantu dalam membuat objek dari templat yang memiliki bidang anggota yang dapat menggantikan lambda secara langsung daripada menggunakan std::functionpembungkus.


3
Terima kasih, itu memang menjelaskan sedikit alasan di balik cara lambda didefinisikan dalam C ++ (saya harus ingat istilah "tipe Voldemort" :-)). Namun, pertanyaannya tetap: Apa keuntungan dari ini di mata seorang desainer bahasa?
cmaster - memulihkan monica

1
Anda bahkan dapat menambahkan int& operator()(){ return x; }ke struct itu
Caleth

2
@ cmaster-reinstatemonica • Secara spekulatif ... C ++ lainnya berperilaku seperti itu. Untuk membuat lambda menggunakan semacam "bentuk permukaan", mengetik bebek akan menjadi sesuatu yang sangat berbeda dari bahasa lainnya. Menambahkan fasilitas semacam itu dalam bahasa lambda mungkin akan dianggap digeneralisasikan untuk seluruh bahasa, dan itu akan menjadi perubahan besar yang berpotensi merusak. Menghilangkan fasilitas seperti itu hanya untuk lambda cocok dengan pengetikan yang kuat dari C ++ lainnya.
Eljay

Secara teknis tipe Voldemort akan auto foo(){ struct DarkLord {} tom_riddle; return tom_riddle; }, karena di luar footidak ada yang dapat menggunakan pengenalDarkLord
Caleth

@ efisiensi cmaster-reinstatemonica, alternatifnya adalah mengotak & secara dinamis mengirimkan setiap lambda (mengalokasikannya di heap dan menghapus jenis tepatnya). Sekarang seperti yang Anda perhatikan, kompilator dapat mendeduplikasi jenis lambda anonim, tetapi Anda masih tidak dapat menuliskannya dan itu akan membutuhkan pekerjaan yang signifikan untuk keuntungan yang sangat kecil, jadi kemungkinannya tidak terlalu menguntungkan.
Masklinn

10

Mengapa merancang bahasa dengan tipe anonim yang unik ?

Karena ada kasus dimana nama menjadi tidak relevan dan tidak berguna atau bahkan kontra produktif. Dalam hal ini kemampuan mengabstraksi keberadaan mereka berguna karena mengurangi polusi nama, dan memecahkan salah satu dari dua masalah sulit dalam ilmu komputer (bagaimana menamai sesuatu). Untuk alasan yang sama, objek sementara berguna.

lambda

Keunikan bukanlah lambda khusus, atau bahkan hal khusus untuk tipe anonim. Ini berlaku untuk tipe bernama dalam bahasa juga. Pertimbangkan berikut ini:

struct A {
    void operator()(){};
};

struct B {
    void operator()(){};
};

void foo(A);

Perhatikan bahwa saya tidak bisa Bmasukfoo , meskipun kelasnya identik. Properti yang sama ini berlaku untuk jenis tanpa nama.

lambda hanya dapat diteruskan ke fungsi templat yang memungkinkan waktu kompilasi, tipe yang tidak dapat disebutkan untuk diteruskan bersama dengan objek ... dihapus melalui std :: function <>.

Ada opsi ketiga untuk subset lambda: Lambda yang tidak menangkap bisa dikonversi menjadi penunjuk fungsi.


Perhatikan bahwa jika batasan tipe anonim merupakan masalah untuk kasus penggunaan, maka solusinya sederhana: Tipe bernama dapat digunakan sebagai gantinya. Lambdas tidak melakukan apa pun yang tidak bisa dilakukan dengan kelas bernama.


10

Jawaban yang diterima Cort Ammon bagus, tetapi saya pikir ada satu hal penting lagi yang harus dibuat tentang implementabilitas.

Misalkan saya memiliki dua unit terjemahan yang berbeda, "one.cpp" dan "two.cpp".

// one.cpp
struct A { int operator()(int x) const { return x+1; } };
auto b = [](int x) { return x+1; };
using A1 = A;
using B1 = decltype(b);

extern void foo(A1);
extern void foo(B1);

Dua kelebihan foopenggunaan menggunakan identifier ( foo) yang sama tetapi memiliki nama yang rusak berbeda. (Dalam Itanium ABI yang digunakan pada sistem POSIX-ish, nama yang rusak adalah _Z3foo1Adan, dalam kasus khusus ini _Z3fooN1bMUliE_E,.)

// two.cpp
struct A { int operator()(int x) const { return x + 1; } };
auto b = [](int x) { return x + 1; };
using A2 = A;
using B2 = decltype(b);

void foo(A2) {}
void foo(B2) {}

Kompilator C ++ harus memastikan bahwa nama rusak void foo(A1)di "two.cpp" sama dengan nama rusak extern void foo(A2)di "one.cpp", sehingga kita dapat menautkan dua file objek bersama-sama. Ini adalah arti fisik dari dua jenis yang menjadi "tipe yang sama": ini pada dasarnya tentang kompatibilitas ABI antara file objek yang dikompilasi secara terpisah.

Compiler C ++ tidak diperlukan untuk memastikan B1dan B2merupakan "tipe yang sama". (Sebenarnya, diperlukan untuk memastikan bahwa mereka berbeda tipe; tapi itu tidak sepenting sekarang.)


Mekanisme fisik apa yang digunakan kompilator untuk memastikan bahwa A1dan A2merupakan "tipe yang sama"?

Itu hanya menggali melalui typedefs, dan kemudian melihat nama tipe yang sepenuhnya memenuhi syarat. Itu adalah tipe kelas bernama A. (Yah, ::Akarena ini ada di namespace global.) Jadi itu tipe yang sama di kedua kasus. Itu mudah dimengerti. Lebih penting lagi, ini mudah diterapkan . Untuk melihat apakah dua tipe kelas adalah tipe yang sama, Anda mengambil namanya dan melakukan a strcmp. Untuk mengacaukan tipe kelas menjadi nama fungsi yang rusak, Anda menulis jumlah karakter dalam namanya, diikuti dengan karakter tersebut.

Jadi, tipe bernama mudah untuk diatur.

Mekanisme fisik apa yang mungkin digunakan compiler untuk memastikan bahwa B1dan B2merupakan "tipe yang sama", dalam dunia hipotetis di mana C ++ mengharuskan mereka untuk menjadi tipe yang sama?

Yah, itu tidak bisa menggunakan nama tipe, karena tipe tidak memiliki nama.

Mungkin entah bagaimana itu bisa menyandikan teks tubuh lambda. Tapi itu akan agak canggung, karena sebenarnya bdi "one.cpp" sedikit berbeda dari bdi "two.cpp": "one.cpp" has x+1dan "two.cpp" has x + 1. Jadi kita harus membuat aturan yang mengatakan bahwa perbedaan whitespace ini tidak penting, atau memang begitu (bagaimanapun juga membuat mereka berbeda tipe), atau mungkin memang begitu (mungkin validitas program ditentukan oleh implementasi , atau mungkin itu "cacat tidak diperlukan diagnostik"). Bagaimanapun,A

Jalan keluar termudah dari kesulitan ini adalah dengan mengatakan bahwa setiap ekspresi lambda menghasilkan nilai dari tipe yang unik. Maka dua jenis lambda yang ditentukan dalam unit terjemahan yang berbeda pasti bukan jenis yang sama . Dalam satu unit terjemahan, kita dapat "memberi nama" jenis lambda hanya dengan menghitung dari awal kode sumber:

auto a = [](){};  // a has type $_0
auto b = [](){};  // b has type $_1
auto f(int x) {
    return [x](int y) { return x+y; };  // f(1) and f(2) both have type $_2
} 
auto g(float x) {
    return [x](int y) { return x+y; };  // g(1) and g(2) both have type $_3
} 

Tentu saja nama-nama ini hanya memiliki arti dalam unit terjemahan ini. TU $_0ini selalu berbeda jenisnya dengan TU lain $_0, meskipun TU struct Aini selalu sama jenisnya dengan TU lain struct A.

Ngomong-ngomong, perhatikan bahwa gagasan "menyandikan teks lambda" kami memiliki masalah halus lainnya: lambda $_2dan $_3terdiri dari teks yang persis sama , tetapi gagasan itu jelas tidak boleh dianggap jenis yang sama !


Ngomong-ngomong, C ++ memang membutuhkan compiler untuk mengetahui cara mengacaukan teks dari ekspresi C ++ arbitrer , seperti pada

template<class T> void foo(decltype(T())) {}
template void foo<int>(int);  // _Z3fooIiEvDTcvT__EE, not _Z3fooIiEvT_

Tapi C ++ tidak (belum) membutuhkan compiler tahu bagaimana mangle sebuah C ++ sewenang-wenang pernyataan . decltype([](){ ...arbitrary statements... })bentuknya masih buruk bahkan dalam C ++ 20.


Juga perhatikan bahwa mudah memberikan alias lokal ke tipe tanpa nama menggunakan typedef/ using. Saya merasa pertanyaan Anda mungkin muncul dari mencoba melakukan sesuatu yang dapat diselesaikan seperti ini.

auto f(int x) {
    return [x](int y) { return x+y; };
}

// Give the type an alias, so I can refer to it within this translation unit
using AdderLambda = decltype(f(0));

int of_one(AdderLambda g) { return g(1); }

int main() {
    auto f1 = f(1);
    assert(of_one(f1) == 2);
    auto f42 = f(42);
    assert(of_one(f42) == 43);
}

DIEDIT UNTUK DITAMBAHKAN: Dari membaca beberapa komentar Anda di jawaban lain, sepertinya Anda bertanya-tanya mengapa

int add1(int x) { return x + 1; }
int add2(int x) { return x + 2; }
static_assert(std::is_same_v<decltype(add1), decltype(add2)>);
auto add3 = [](int x) { return x + 3; };
auto add4 = [](int x) { return x + 4; };
static_assert(not std::is_same_v<decltype(add3), decltype(add4)>);

Itu karena lambda yang tidak dapat ditangkap dapat dibangun secara default. (Dalam C ++ hanya pada C ++ 20, tetapi itu selalu benar secara konseptual .)

template<class T>
int default_construct_and_call(int x) {
    T t;
    return t(x);
}

assert(default_construct_and_call<decltype(add3)>(42) == 45);
assert(default_construct_and_call<decltype(add4)>(42) == 46);

Jika Anda mencoba default_construct_and_call<decltype(&add1)>, takan menjadi penunjuk fungsi yang diinisialisasi default dan Anda mungkin akan segfault. Itu, sepertinya, tidak berguna.


Faktanya, diperlukan untuk memastikan bahwa mereka berbeda tipe; tapi itu tidak sepenting sekarang. ” Saya ingin tahu apakah ada alasan yang baik untuk memaksakan keunikan jika didefinisikan secara ekuivalen.
Deduplicator

Secara pribadi saya pikir perilaku yang didefinisikan sepenuhnya (hampir?) Selalu lebih baik daripada perilaku yang tidak ditentukan. "Apakah kedua penunjuk fungsi ini sama? Nah, hanya jika kedua contoh template ini adalah fungsi yang sama, yang benar hanya jika kedua jenis lambda ini adalah jenis yang sama, yang hanya berlaku jika kompilator memutuskan untuk menggabungkannya." Menjijikkan! (Tapi pemberitahuan bahwa kita memiliki persis situasi analog dengan penggabungan string literal, dan tidak ada yang terganggu tentang bahwa situasi Jadi aku ragu itu akan menjadi bencana untuk mengizinkan compiler untuk menggabungkan jenis identik..)
Quuxplusone

Nah, apakah dua fungsi yang setara (kecuali seolah-olah) mungkin identik juga merupakan pertanyaan yang bagus. Bahasa dalam standar tidak terlalu jelas untuk fungsi bebas dan / atau statis. Tapi itu di luar cakupan di sini.
Deduplicator

Kebetulan, telah ada diskusi bulan ini sangat pada LLVM milis tentang penggabungan fungsi. Codegen Clang akan menyebabkan fungsi dengan body yang benar-benar kosong digabungkan hampir "secara tidak sengaja": godbolt.org/z/obT55b Ini secara teknis tidak sesuai, dan saya pikir mereka akan menambal LLVM untuk berhenti melakukan ini. Tapi ya, setuju, menggabungkan alamat fungsi juga merupakan suatu hal.
Quuxplusone

Contoh tersebut memiliki masalah lain yaitu missing return-statement. Bukankah mereka sendiri sudah membuat kode tidak sesuai? Juga, saya akan mencari pembahasannya, tetapi apakah mereka menunjukkan atau menganggap penggabungan fungsi yang setara tidak sesuai dengan standar, perilaku mereka yang terdokumentasi, ke gcc, atau hanya beberapa mengandalkan itu tidak terjadi?
Deduplicator

9

C ++ lambda membutuhkan tipe yang berbeda untuk operasi yang berbeda, karena C ++ mengikat secara statis. Mereka hanya dapat disalin / dipindah-pindah, jadi kebanyakan Anda tidak perlu memberi nama tipenya. Tapi itu semua adalah detail implementasi.

Saya tidak yakin apakah C # lambda memiliki tipe, karena mereka adalah "ekspresi fungsi anonim", dan mereka segera dikonversi ke tipe delegasi yang kompatibel atau tipe pohon ekspresi. Jika ya, itu mungkin jenis yang tidak dapat diucapkan.

C ++ juga memiliki struct anonim, di mana setiap definisi mengarah ke tipe yang unik. Di sini namanya tidak dapat dilafalkan, itu sama sekali tidak ada sejauh menyangkut standar.

C # memiliki tipe data anonim , yang dengan hati-hati melarangnya keluar dari cakupan yang mereka definisikan. Implementasinya juga memberikan nama yang unik dan tidak dapat diucapkan untuk itu.

Memiliki tipe anonim memberi sinyal kepada programmer bahwa mereka tidak boleh melihat-lihat dalam implementasinya.

Ke samping:

Anda dapat memberi nama untuk jenis lambda.

auto foo = []{}; 
using Foo_t = decltype(foo);

Jika Anda tidak memiliki tangkapan apa pun, Anda bisa menggunakan tipe penunjuk fungsi

void (*pfoo)() = foo;

1
Kode contoh pertama masih tidak mengizinkan yang berikutnya Foo_t = []{};, hanya Foo_t = foodan tidak ada yang lain.
cmaster - memulihkan monica

1
@ cmaster-reinstatemonica itu karena jenisnya tidak dapat dibangun secara default, bukan karena anonimitas. Dugaan saya adalah hal itu berkaitan dengan menghindari memiliki set kasing sudut yang lebih besar yang harus Anda ingat, karena alasan teknis apa pun.
Caleth

6

Mengapa menggunakan tipe anonim?

Untuk tipe yang secara otomatis dihasilkan oleh kompilator, pilihannya adalah (1) menghormati permintaan pengguna untuk nama tipe tersebut, atau (2) membiarkan kompilator memilihnya sendiri.

  1. Dalam kasus sebelumnya, pengguna diharapkan memberikan nama secara eksplisit setiap kali konstruksi seperti itu muncul (C ++ / Rust: setiap kali lambda didefinisikan; Rust: setiap kali fungsi didefinisikan). Ini adalah detail yang membosankan untuk diberikan pengguna setiap saat, dan dalam sebagian besar kasus, nama tersebut tidak pernah dirujuk lagi. Oleh karena itu, masuk akal untuk membiarkan compiler mencari nama untuk itu secara otomatis, dan menggunakan fitur yang ada seperti decltypeinferensi atau tipe untuk mereferensikan tipe di beberapa tempat yang dibutuhkan.

  2. Dalam kasus terakhir, kompilator perlu memilih nama unik untuk tipe tersebut, yang mungkin merupakan nama yang tidak jelas dan tidak dapat dibaca seperti __namespace1_module1_func1_AnonymousFunction042. Perancang bahasa dapat menentukan dengan tepat bagaimana nama ini dibuat dengan detail yang indah dan halus, tetapi ini tidak perlu memperlihatkan detail implementasi kepada pengguna yang tidak dapat diandalkan oleh pengguna yang bijaksana, karena nama tersebut tidak diragukan lagi rapuh dalam menghadapi refaktor kecil sekalipun. Ini juga tidak perlu membatasi evolusi bahasa: penambahan fitur di masa mendatang dapat menyebabkan algoritme pembuatan nama yang ada berubah, yang menyebabkan masalah kompatibilitas ke belakang. Oleh karena itu, masuk akal untuk mengabaikan detail ini, dan menegaskan bahwa jenis yang dibuat secara otomatis tidak dapat diutamakan oleh pengguna.

Mengapa menggunakan tipe unik (berbeda)?

Jika suatu nilai memiliki tipe unik, maka compiler pengoptimal dapat melacak tipe unik di semua situs penggunaannya dengan jaminan ketepatan. Sebagai akibatnya, pengguna kemudian dapat yakin di tempat-tempat di mana asal dari nilai khusus ini diketahui kompilator.

Sebagai contoh, saat kompilator melihat:

let f: __UniqueFunc042 = || { ... };  // definition of __UniqueFunc042 (assume it has a nontrivial closure)

/* ... intervening code */

let g: __UniqueFunc042 = /* some expression */;
g();

compiler memiliki keyakinan penuh yang gharus berasal dari f, bahkan tanpa mengetahui asal dari g. Ini akan memungkinkan panggilan untuk gdidevirtualisasi. Pengguna akan mengetahui hal ini juga, karena pengguna telah sangat berhati-hati untuk mempertahankan tipe unik fmelalui aliran data yang mengarah ke g.

Seharusnya, ini membatasi apa yang dapat dilakukan pengguna f. Pengguna tidak bebas menulis:

let q = if some_condition { f } else { || {} };  // ERROR: type mismatch

karena hal itu akan mengarah pada penyatuan (ilegal) dua jenis yang berbeda.

Untuk mengatasi ini, pengguna dapat menyalurkan __UniqueFunc042ke tipe non-unik &dyn Fn(),

let f2 = &f as &dyn Fn();  // upcast
let q2 = if some_condition { f2 } else { &|| {} };  // OK

Kompromi yang dibuat oleh penghapusan jenis ini adalah penggunaan &dyn Fn()alasan yang rumit bagi kompilator. Diberikan:

let g2: &dyn Fn() = /*expression */;

kompilator harus dengan susah payah memeriksa /*expression */untuk menentukan apakah g2berasal dari fatau beberapa fungsi lain, dan kondisi di mana asalnya berlaku. Dalam banyak situasi, kompilator mungkin menyerah: mungkin manusia dapat mengatakan bahwa itu g2benar - benar datang dari fdalam semua situasi tetapi jalur dari fke g2terlalu berbelit-belit untuk diuraikan oleh kompilator, mengakibatkan panggilan virtual ke g2dengan kinerja pesimis.

Ini menjadi lebih jelas ketika objek seperti itu dikirim ke fungsi generik (template):

fn h<F: Fn()>(f: F);

Jika seseorang memanggil h(f)where f: __UniqueFunc042, then hdikhususkan untuk instance unik:

h::<__UniqueFunc042>(f);

Hal ini memungkinkan kompilator untuk menghasilkan kode khusus untuk h, disesuaikan untuk argumen tertentu f, dan pengiriman ke fkemungkinan besar bersifat statis, jika tidak sebaris.

Dalam skenario sebaliknya, di mana seseorang memanggil h(f)dengan f2: &Fn(), yang hdipakai sebagai

h::<&Fn()>(f);

yang dibagi di antara semua fungsi tipe &Fn(). Dari dalam h, kompilator hanya mengetahui sedikit tentang fungsi jenis yang tidak tembus cahaya &Fn()dan karenanya hanya dapat memanggil secara konservatif fdengan pengiriman virtual. Untuk mengirimkan secara statis, compiler harus melakukan panggilan inline ke h::<&Fn()>(f)di situs panggilannya, yang tidak dijamin jika hterlalu rumit.


Bagian pertama tentang memilih nama meleset dari intinya: Tipe seperti void(*)(int, double)mungkin tidak memiliki nama, tetapi saya dapat menuliskannya. Saya akan menyebutnya tipe tanpa nama, bukan tipe anonim. Dan saya akan menyebut hal-hal samar seperti __namespace1_module1_func1_AnonymousFunction042nama mangling, yang jelas tidak termasuk dalam cakupan pertanyaan ini. Pertanyaan ini adalah tentang tipe-tipe yang dijamin oleh standar tidak mungkin untuk ditulis, sebagai kebalikan dari memperkenalkan sintaks tipe yang bisa mengekspresikan tipe-tipe ini dengan cara yang berguna.
cmaster - memulihkan monica

3

Pertama, lambda tanpa tangkapan dapat diubah menjadi penunjuk fungsi. Jadi mereka memberikan beberapa bentuk kemurahan hati.

Sekarang mengapa lambda dengan capture tidak dapat diubah menjadi pointer? Karena fungsi harus mengakses status lambda, jadi status ini perlu muncul sebagai argumen fungsi.


Nah, hasil tangkapan seharusnya menjadi bagian dari lambda itu sendiri, bukan? Sama seperti mereka dikemas dalam file std::function<>.
cmaster - memulihkan monica

3

Untuk menghindari benturan nama dengan kode pengguna.

Bahkan dua lambda dengan implementasi yang sama akan memiliki tipe yang berbeda. Tidak apa-apa karena saya juga dapat memiliki jenis objek yang berbeda meskipun tata letak memorinya sama.


Jenis seperti int (*)(Foo*, int, double)tidak memiliki risiko benturan nama dengan kode pengguna.
cmaster - memulihkan monica

Teladan Anda tidak bisa digeneralisasi dengan baik. Meskipun ekspresi lambda hanya sintaksis, ekspresi lambda akan mengevaluasi beberapa struct terutama dengan klausa penangkapan. Menamainya secara eksplisit dapat menyebabkan bentrokan nama pada struct yang sudah ada.
knivil

Sekali lagi, pertanyaan ini tentang desain bahasa, bukan tentang C ++. Saya pasti bisa mendefinisikan bahasa di mana tipe lambda lebih mirip dengan tipe penunjuk fungsi daripada tipe struktur data. Sintaks penunjuk fungsi dalam C ++ dan sintaks tipe larik dinamis di C membuktikan bahwa hal ini dimungkinkan. Dan itu menimbulkan pertanyaan, mengapa lambda tidak menggunakan pendekatan serupa?
cmaster - memulihkan monica

1
Tidak, Anda tidak bisa, karena variasi kari (menangkap). Anda membutuhkan fungsi dan data untuk membuatnya berfungsi.
Blindy

@ Blindy Oh, ya, saya bisa. Saya bisa mendefinisikan lambda menjadi objek yang berisi dua pointer, satu untuk objek tangkap, dan satu lagi untuk kode. Objek lambda seperti itu akan mudah diedarkan dengan nilai. Atau saya bisa menarik trik dengan potongan kode di awal objek penangkapan yang mengambil alamatnya sendiri sebelum beralih ke kode lambda yang sebenarnya. Itu akan mengubah penunjuk lambda menjadi satu alamat. Tapi itu tidak perlu karena platform PPC telah membuktikan: Di PPC, penunjuk fungsi sebenarnya adalah sepasang penunjuk. Itulah mengapa Anda tidak dapat melakukan cast void(*)(void)ke void*dan kembali dalam C / C ++ standar.
cmaster - memulihkan monica
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.