Neden benzersiz anonim türlere sahip bir dil tasarlayasınız?


91

Bu, beni her zaman C ++ lambda ifadelerinin bir özelliği olarak rahatsız eden bir şeydir: Bir C ++ lambda ifadesinin türü benzersiz ve anonimdir, basitçe yazamam. Sözdizimsel olarak tamamen aynı olan iki lambda oluştursam bile, ortaya çıkan türler farklı olarak tanımlanır. Sonuç şudur: a) lambdalar yalnızca derleme zamanına, konuşulamayan türün nesneyle birlikte aktarılmasına izin veren şablon işlevlerine geçirilebilir ve b) lambdalar yalnızca türler aracılığıyla silindikten sonra yararlıdır std::function<>.

Tamam, ama C ++ 'nın yaptığı gibi, onu sadece o dilin rahatsız edici bir özelliği olarak yazmaya hazırdım. Ancak, Rust'un görünüşte aynı şeyi yaptığını öğrendim: Her Rust işlevi veya lambda benzersiz, anonim bir türe sahiptir. Ve şimdi merak ediyorum: Neden?

Öyleyse sorum şu:
Bir dil tasarımcısı bakış açısından, benzersiz, anonim bir tür kavramını bir dile sokmanın avantajı nedir?


6
her zaman olduğu gibi daha iyi soru neden olmasın.
Stargateur

31
"bu lambdalar yalnızca std :: function <> aracılığıyla tür silindikten sonra yararlıdır" - hayır, olmadan doğrudan yararlıdırlar std::function. Bir şablon işlevine iletilen bir lambda, dahil edilmeden doğrudan çağrılabilir std::function. Derleyici daha sonra çalışma zamanı verimliliğini artıracak şekilde lambda'yı şablon işlevine satır içi yapabilir.
Erlkoenig

1
Tahminim lambda'nın uygulanmasını kolaylaştırıyor ve dilin anlaşılmasını kolaylaştırıyor. Tam olarak aynı lambda ifadesinin aynı türe katlanmasına izin { int i = 42; auto foo = [&i](){ return i; }; } { int i = 13; auto foo = [&i](){ return i; }; }verdiyseniz, metin olarak aynı olsalar bile, başvurduğu değişken farklı olduğundan, işlemek için özel kurallara ihtiyacınız olacaktır . Sadece hepsinin benzersiz olduğunu söylerseniz, anlamaya çalışmaktan endişelenmenize gerek yok.
NathanOliver

5
ancak bir lambdas türüne de bir isim verebilir ve bununla aynısını yapabilirsiniz. lambdas_type = decltype( my_lambda);
most_prime_is_463035818

3
Fakat genel bir lambda türü ne olmalıdır [](auto) {}? Başlamak için bir türü olmalı mı?
Evg

Yanıtlar:


77

Birçok standart (özellikle C ++), derleyicilerden ne kadar talep ettiklerini en aza indirme yaklaşımını benimser. Açıkçası zaten yeterince talep ediyorlar! Çalışması için bir şey belirtmeleri gerekmiyorsa, onu uygulama tanımlı bırakma eğilimindedirler.

Lambdalar anonim kalmasaydı, onları tanımlamamız gerekirdi. Bu, değişkenlerin nasıl yakalandığı hakkında çok şey söylemelidir. Bir lambda durumunu düşünün [=](){...}. Tür, lambda tarafından gerçekte hangi türlerin yakalandığını belirtmek zorunda kalacaktı, bu da belirlemek için önemsiz olmayabilir. Ayrıca, derleyici bir değişkeni başarıyla optimize ederse ne olur? Düşünmek:

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

Optimize edici bir derleyici, iyakalanabilecek tek olası değerin 5 olduğunu kolayca anlayabilir ve bunun yerine auto f = []() { return 5; }. Ancak, tür anonim değilse, bu, türü değiştirebilir veya derleyiciyi daha az optimize etmeye zorlayabilir, iaslında ihtiyacı olmasa bile depolayabilir . Bu, lambdaların yapması amaçlanan şey için gerekli olmayan bir karmaşıklık ve nüans çantasıdır.

Ve aslında anonim olmayan bir türe ihtiyaç duyduğunuz kapalı durumda, her zaman kapatma sınıfını kendiniz oluşturabilir ve bir lambda işlevi yerine bir functor ile çalışabilirsiniz. Böylece, lambdaların% 99 durumunu ele almasını sağlayabilir ve sizi% 1'de kendi çözümünüzü kodlamaya bırakabilirler.


Deduplicator, yorumlarda anonimlik kadar benzersizliğe değinmediğime dikkat çekti. Benzersizliğin faydalarından daha az eminim, ancak türler benzersizse aşağıdaki davranışların açık olduğunu belirtmek gerekir (eylem iki kez örneklenecektir).

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

Türler benzersiz olmasaydı, bu durumda hangi davranışın olması gerektiğini belirtmemiz gerekirdi. Bu aldatıcı olabilir. İsimsizlik konusunda gündeme gelen bazı konular, bu durumda benzersizlik için çirkin yüzlerini ortaya çıkarıyor.


Bunun bir derleyici uygulayıcısı için işten tasarruf etmekle ilgili olmadığını, standartları koruyan için işten tasarruf etmekle ilgili olduğunu unutmayın. Derleyicinin yine de özel uygulaması için yukarıdaki tüm soruları yanıtlaması gerekir, ancak bunlar standartta belirtilmemiştir.
ComicSansMS

2
@ComicSansMS Bir derleyiciyi uygularken bu tür şeyleri bir araya getirmek, uygulamanızı başka birinin standardına uydurmak zorunda olmadığınızda çok daha kolaydır. Deneyimlerden bahsedersek , bir standart geliştirici için işlevselliği aşırı belirtmek, dilinizden istenen işlevselliği elde etmeye devam ederken belirtilecek minimum miktarı bulmaya çalışmaktan çok daha kolaydır. Mükemmel bir örnek olay incelemesi olarak, bellek_sırası_ tüketimini fazla belirtmekten kaçınmak için ne kadar iş harcadıklarına bakın ve yine de onu yararlı hale getirdiklerine bakın (bazı mimarilerde)
Cort Ammon

1
Herkes gibi, anonim olmak için ikna edici bir dava açarsınız . Ama onu benzersiz olmaya zorlamak gerçekten çok iyi bir fikir mi?
Deduplicator

Burada önemli olan derleyicinin karmaşıklığı değil, üretilen kodun karmaşıklığıdır. Önemli olan, derleyiciyi basitleştirmek değil, tüm durumları optimize etmek ve hedef platform için doğal kod üretmek için ona yeterli hareket alanı sağlamaktır.
Jan Hudec

Statik bir değişkeni yakalayamazsınız.
Ruslan

69

Lambdalar sadece işlevler değil, bir işlev ve bir durumdur . Bu nedenle, hem C ++ hem de Rust, bunları bir çağrı operatörüyle bir nesne olarak uygular ( operator()C ++ 'da, Fn*Rust'taki 3 özellik).

Temel olarak, [a] { return a + 1; }C ++ 'da şuna benzer

struct __SomeName {
    int a;

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

daha sonra __SomeNamelambda'nın kullanıldığı bir örnek kullanarak .

|| a + 1Rust'tayken , in Rust gibi bir şeye

{
    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 }
}

Bu, çoğu lambdanın farklı türlere sahip olması gerektiği anlamına gelir .

Şimdi, bunu yapmanın birkaç yolu var:

  • Anonim türlerle, her iki dilin de uyguladığı şey budur. Bunun bir başka sonucu da tüm lambdaların farklı bir türe sahip olması gerektiğidir . Ancak dil tasarımcıları için bunun açık bir avantajı vardır: Lambdas, dilin halihazırda var olan diğer daha basit bölümleri kullanılarak basitçe tanımlanabilir. Bunlar, dilin zaten var olan parçalarının etrafındaki sözdizimi şekeridir.
  • Lambda türlerini adlandırmak için bazı özel sözdizimi ile: Bu, ancak gerekli değildir çünkü lambdalar C ++ 'daki şablonlarla veya genel ve Rust'taki Fn*özelliklerle kullanılabilir. Her iki dil de sizi lambdaları kullanmak için yazıp silmeye zorlamaz ( std::functionC ++ veya Box<Fn*>Rust'ta).

Ayrıca her iki dilde yapamaz yakalama bağlamını yapmak önemsiz lambda'lar kabul dikkat edebilirsiniz işlev işaretçileri dönüştürülebilir.


Daha basit bir özellik kullanarak bir dilin karmaşık özelliklerini tanımlamak oldukça yaygındır. Örneğin, hem C ++ hem de Rust, aralık için döngülere sahiptir ve her ikisi de bunları diğer özellikler için sözdizimi şekeri olarak tanımlar.

C ++ tanımlar

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

eşdeğer olarak

{

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

        range_declaration = *__begin;
        loop_statement

    }

} 

ve Rust tanımlar

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

eşdeğer olarak

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

bir insan için daha karmaşık görünseler de, hem bir dil tasarımcısı hem de bir derleyici için daha basittir.


15
@ cmaster-reinstatemonica Bir sıralama işlevi için karşılaştırıcı argüman olarak bir lambda geçirmeyi düşünün. Gerçekten buraya sanal işlev çağrılarının ek yükünü empoze etmek ister miydiniz?
Daniel Langr

5
@ cmaster-reinstatemonica çünkü C ++ 'da hiçbir şey varsayılan olarak sanal değildir
Caleth

4
@cmaster - Tüm lambda kullanıcılarını ihtiyaç duymadıklarında bile dinamik dipatch için ödeme yapmaya zorlamak mı istiyorsunuz?
StoryTeller - Unslander Monica

4
@ cmaster-reinstatemonica Elde edeceğiniz en iyi şey, sanal olarak kaydolmaktır. Ne Tahmin, std::functionbunu yapmaz
Caleth

9
@ cmaster-reinstatemonica Çağrılacak işlevi yeniden işaretleyebileceğiniz herhangi bir mekanizma, çalışma zamanı ek yükü olan durumlara sahip olacaktır. Bu C ++ yolu değildir. Sen İle optstd::function
Caleth

13

(Caleth'in cevabına ekleniyor, ancak yoruma sığmayacak kadar uzun.)

Lambda ifadesi, anonim bir yapı için sözdizimsel bir şekerdir (bir Voldemort türü, çünkü adını söyleyemezsiniz).

Bu kod pasajında ​​anonim bir yapı ile bir lambda anonimliği arasındaki benzerliği görebilirsiniz:

#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";
}

Bu bir lambda için hala tatmin edici değilse, anonim bir yapı için de aynı şekilde tatmin edici olmamalıdır.

Bazı diller, biraz daha esnek olan bir tür ördek yazımına izin verir ve C ++, bir lambda kullanmak yerine doğrudan bir lambda değiştirebilen bir şablondan bir nesne oluşturmaya gerçekten yardımcı olmayan şablonlara sahip olsa da std::functionsarmalayıcı.


3
Teşekkürler, bu gerçekten lambdaların C ++ 'da tanımlanma şeklinin arkasındaki mantığa biraz ışık tutuyor ("Voldemort türü" terimini hatırlamam gerekiyor :-)). Bununla birlikte, şu soru kalır: Bir dil tasarımcısının gözünde bunun avantajı nedir ?
cmaster -

1
Bu int& operator()(){ return x; }yapılara bile ekleyebilirsiniz
Caleth

2
@ cmaster-reinstatemonica • Spekülatif olarak ... C ++ 'ın geri kalanı bu şekilde davranır. Lambdaların bir çeşit "yüzey şekli" kullanmasını sağlamak için ördek yazımı, dilin geri kalanından çok farklı bir şey olurdu. Lambdas diline bu tür bir kolaylık eklemek, muhtemelen tüm dil için genelleştirilmiş olarak kabul edilir ve bu, potansiyel olarak büyük bir kırılma değişikliği olur. Sadece lambdalar için böyle bir tesisin çıkarılması, C ++ 'ın geri kalanının güçlü bir şekilde yazılmasına uymaktadır.
Eljay

Teknik olarak bir Voldemort türü olabilir auto foo(){ struct DarkLord {} tom_riddle; return tom_riddle; }, çünkü foohiçbir şeyin dışında tanımlayıcıyı kullanamazDarkLord
Caleth

@ cmaster-reinstatemonica verimliliği, alternatifi her lambdayı kutulamak ve dinamik olarak göndermek olabilir (onu yığına ayırın ve kesin türünü silin). Şimdi dikkat olarak derleyici olabilir lambdas anonim türleri deduplicate, ama hala onları yazmaya mümkün olmaz ve oranlar lehine gerçekten değil bu yüzden, çok az kazanç için önemli çalışmalar gerektirecektir.
Masklinn

10

Neden benzersiz anonim türlere sahip bir dil tasarlayasınız ?

Çünkü isimlerin alakasız olduğu ve yararlı olmadığı ve hatta ters etki yarattığı durumlar vardır. Bu durumda, varlıklarını soyutlama yeteneği yararlıdır çünkü isim kirliliğini azaltır ve bilgisayar bilimindeki iki zor problemden birini (şeylerin nasıl adlandırılacağı) çözer. Aynı nedenle, geçici nesneler kullanışlıdır.

lambda

Benzersizlik, özel bir lambda şeyi, hatta anonim tipler için özel bir şey değildir. Dildeki adlandırılmış türler için de geçerlidir. Aşağıdakileri düşünün:

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

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

void foo(A);

Ben geçemediği Not Biçine foosınıfları aynı olsa bile,. Aynı özellik adsız türler için de geçerlidir.

lambdalar yalnızca derleme zamanının, konuşulamayan türlerin std :: function <> ile silinen nesneyle birlikte geçirilmesine izin veren şablon işlevlerine geçirilebilir.

Lambdaların bir alt kümesi için üçüncü bir seçenek vardır: Yakalamayan lambdalar işlev işaretçilerine dönüştürülebilir.


Anonim bir türün sınırlamaları bir kullanım durumu için sorun oluşturuyorsa, çözüm basittir: Bunun yerine adlandırılmış bir tür kullanılabilir. Lambdas, adlandırılmış bir sınıfla yapılamayacak hiçbir şey yapmaz.


10

Cort Ammon'un kabul ettiği cevap iyi, ancak uygulanabilirlikle ilgili yapılması gereken bir önemli nokta daha var.

İki farklı çeviri birimim olduğunu varsayalım, "one.cpp" ve "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);

İki aşırı yükleme fooaynı tanımlayıcıyı ( foo) kullanır ancak farklı karışık adlara sahiptir. (POSIX-ish sistemlerde kullanılan Itanium ABI'de, karıştırılmış isimler _Z3foo1Ave bu özel durumda _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) {}

C ++ derleyicisi gerekir ait parçalanmış adı sağlamak void foo(A1)"two.cpp" içinde mangled adıyla aynıdır extern void foo(A2)Birlikte iki nesne dosyalarını bağlayabilirsiniz böylece, "one.cpp" in. Bu, "aynı tür" olan iki türün fiziksel anlamıdır : temelde ayrı olarak derlenen nesne dosyaları arasındaki ABI uyumluluğu ile ilgilidir.

C ++ derleyicisi olduğu değil emin olmak için gerekli B1ve B2vardır "Aynı tip." (Aslında, farklı tipte olmalarını sağlamak gerekiyor; ancak bu şu anda o kadar önemli değil.)


Derleyici bunu sağlamak için hangi fiziksel mekanizmayı kullanıyor A1ve A2"aynı tip" mi?

Basitçe yazım biçimlerini inceler ve ardından türün tam olarak nitelenmiş adına bakar. Adlı bir sınıf türüdür A. ( ::AKüresel isim alanında olduğu için.) Yani her iki durumda da aynı tip. Anlaması kolay. Daha da önemlisi, uygulanması kolaydır . İki sınıf türünün aynı tür olup olmadığını görmek için adlarını alır ve bir strcmp. Bir sınıf türünü bir işlevin karıştırılmış ismine karıştırmak için, onun adına karakter sayısını ve ardından bu karakterleri yazarsınız.

Yani, adlandırılmış türlerin karıştırılması kolaydır.

Ne fiziksel mekanizma olabileceğini derleyici kullanımı sağlamak için B1ve B2C ++ aynı tip olmalarını gerektiğinde "aynı tip," varsayımsal dünyada nelerdir?

Tipi değil çünkü Eh, türünün adını kullanamadı sahip bir isim.

Belki bir şekilde lambda gövdesinin metnini kodlayabilirdi . Ama bu biraz garip olurdu, çünkü aslında b"one.cpp" içindeki "two.cpp" deki biraz farklı b: "one.cpp" x+1ve "two.cpp" var x + 1. Bu boşluk fark ya şeklindeki bir kural ile gelip olurdu Yani yok olsun ya da o yapar (sonuçta onları farklı yapmak) veya bu belki öyle (belki programın geçerlilik uygulaması-tanımlanır veya belki de "teşhis gerektirmeyen kötü biçimlidir"). Her neyse,A

Zorluktan çıkmanın en kolay yolu, her lambda ifadesinin benzersiz tipte değerler ürettiğini söylemektir. O zaman farklı çeviri birimlerinde tanımlanan iki lambda türü kesinlikle aynı tür değildir . Tek bir çeviri biriminde, lambda türlerini yalnızca kaynak kodun başından itibaren sayarak "adlandırabiliriz":

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
} 

Elbette bu isimlerin sadece bu çeviri birimi içinde anlamı vardır. Bu OG'ler $_0her zaman diğer bazı TU'larla aynı tipte $_0olsa da, bu OG'ler struct Aher zaman diğer bazı OG'lerden farklı bir tiptir struct A.

Bu arada, "lambda metnini kodla" fikrimizin başka bir ince problemi olduğuna dikkat edin: lambdas $_2ve $_3tamamen aynı metinden oluşuyor , ancak açıkça aynı tür olarak kabul edilmemeleri gerekiyor !


Bu arada, C ++, derleyicinin rasgele bir C ++ ifadesinin metnini nasıl karıştıracağını bilmesini gerektirir .

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

Ancak C ++, derleyicinin rastgele bir C ++ ifadesini nasıl karıştıracağını bilmesini gerektirmez (henüz) . decltype([](){ ...arbitrary statements... })C ++ 20'de bile hala biçimsiz.


Ayrıca / kullanarak adsız bir türe yerel bir takma ad vermenin kolay olduğuna dikkat edin . Sorunuzun bu şekilde çözülebilecek bir şey yapmaya çalışmaktan kaynaklanmış olabileceğine dair bir his var.typedefusing

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

EKLENMEK İÇİN DÜZENLENMİŞTİR: Diğer cevaplarla ilgili yorumlarınızın bazılarını okurken, nedenini merak ediyormuşsunuz gibi geliyor

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)>);

Bunun nedeni, yakalamasız lambdaların varsayılan olarak oluşturulabilir olmasıdır. (C ++ 'da yalnızca C ++ 20'den itibaren, ancak kavramsal olarak her zaman doğru olmuştur.)

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);

Eğer denediyseniz default_construct_and_call<decltype(&add1)>, tvarsayılan olarak başlatılmış bir işlev işaretçisi olur ve muhtemelen segfault olursunuz. Faydalı değil gibi.


" Aslında, farklı tipte olmalarını sağlamak gerekiyor; ancak bu şu anda o kadar önemli değil. " Eşdeğer olarak tanımlanırsa benzersizliği zorlamak için iyi bir neden olup olmadığını merak ediyorum.
Deduplicator

Kişisel olarak, tamamen tanımlanmış davranışın (neredeyse?) Belirsiz davranıştan her zaman daha iyi olduğunu düşünüyorum. "Bu iki işlev işaretçisi eşit mi? Peki, yalnızca bu iki şablon somutlaştırması aynı işlevse, bu yalnızca bu iki lambda türü aynı türse doğrudur, bu yalnızca derleyici onları birleştirmeye karar verirse doğrudur." Icky! (Ancak dizgi-literal birleştirme ile tam olarak benzer bir durumumuz olduğuna dikkat edin ve bu durumdan kimse rahatsız değil . Bu yüzden derleyicinin aynı türleri birleştirmesine izin
vermenin

Peki, iki eşdeğer işlevin (sanki dışında) aynı olup olmayacağı da güzel bir soru. Standarttaki dil, özgür ve / veya statik işlevler için pek açık değildir. Ama buradaki kapsam dışında.
Deduplicator

Şans eseri, bu ay LLVM posta listesinde birleştirme işlevleriyle ilgili tartışmalar oldu . Clang'ın kod oluşturucusu , tamamen boş gövdeli işlevlerin neredeyse "kazara" birleştirilmesine neden olacaktır: godbolt.org/z/obT55b Bu teknik olarak uygun değildir ve bence bunu yapmayı bırakmak için LLVM'ye yama yapacaklar. Ama evet, kabul etti, fonksiyon adreslerini birleştirmek de bir şey.
Quuxplusone

Bu örnekte, eksik dönüş ifadesi gibi başka sorunlar da var. Tek başlarına kodu uyumsuz hale getirmiyorlar mı? Ayrıca, tartışmayı arayacağım, ancak eşdeğer işlevleri birleştirmenin standarda, belgelenmiş davranışlarına, gcc'ye uymadığını ya da sadece bazılarının buna güvendiğini gösterdiler veya varsaydılar mı?
Deduplicator

9

C ++ statik olarak bağlandığından, C ++ lambdalar farklı işlemler için farklı türlere ihtiyaç duyar . Bunlar yalnızca kopyala / taşı-yapılandırılabilir, bu nedenle çoğunlukla türlerini adlandırmanıza gerek yoktur. Ama hepsi bir şekilde bir uygulama detayı.

C # lambdaların "anonim işlev ifadeleri" olduklarından ve hemen uyumlu bir temsilci türüne veya ifade ağacı türüne dönüştürüldükleri için bir türe sahip olup olmadığından emin değilim. Eğer öyleyse, muhtemelen telaffuz edilemeyen bir türdür.

C ++ ayrıca her tanımın benzersiz bir türe yol açtığı anonim yapılara sahiptir. Burada isim telaffuz edilemez değil, standart söz konusu olduğu sürece mevcut değil.

C # anonim veri türlerine sahiptir ve bu türler , tanımlandıkları kapsamdan çıkmalarını dikkatle yasaklar. Uygulama, bunlara da benzersiz, telaffuz edilemez bir isim verir.

Anonim bir türe sahip olmak, programcıya uygulamalarının içine girmemesi gerektiğini gösterir.

Kenara:

Sen edebilir bir lambda en türüne bir ad verin.

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

Herhangi bir yakalamaya sahip değilseniz, bir işlev işaretçisi türü kullanabilirsiniz.

void (*pfoo)() = foo;

1
İlk örnek kod Foo_t = []{};, yalnızca bir sonrakine izin vermez , Foo_t = foobaşka hiçbir şeye izin vermez .
cmaster

1
@ cmaster-reinstatemonica bunun nedeni, türün anonimlik nedeniyle değil, varsayılan olarak yapılandırılabilir olmamasıdır. Benim tahminim, herhangi bir teknik neden olarak hatırlamanız gereken daha büyük bir köşe kasası setinden kaçınmakla ilgili.
Caleth

6

Neden anonim türleri kullanmalı?

Derleyici tarafından otomatik olarak oluşturulan türler için seçim, (1) bir kullanıcının türün adı için isteğini yerine getirmek veya (2) derleyicinin kendi başına birini seçmesine izin vermektir.

  1. İlk durumda, kullanıcının böyle bir yapı her göründüğünde açıkça bir ad vermesi beklenir (C ++ / Rust: bir lambda tanımlandığında; Rust: bir işlev tanımlandığında). Bu, kullanıcının her seferinde vermesi gereken yorucu bir ayrıntıdır ve çoğu durumda addan bir daha asla bahsedilmez. Bu nedenle, derleyicinin onun için otomatik olarak bir isim bulmasına izin vermek ve decltypeihtiyaç duyulan birkaç yerde türe başvurmak için veya tür çıkarımı gibi mevcut özellikleri kullanmasına izin vermek mantıklıdır .

  2. İkinci durumda, derleyicinin tür için benzersiz bir ad seçmesi gerekir; bu muhtemelen belirsiz, okunamayan bir ad olacaktır __namespace1_module1_func1_AnonymousFunction042. Dil tasarımcısı, bu ismin nasıl muhteşem ve hassas ayrıntılarla nasıl inşa edildiğini tam olarak belirleyebilir, ancak bu, kullanıcıya hiçbir mantıklı kullanıcının güvenemeyeceği bir uygulama ayrıntısını gereksiz yere ifşa eder, çünkü ad, küçük refaktörler karşısında bile hiç şüphesiz kırılgandır. Bu aynı zamanda dilin gelişimini gereksiz yere kısıtlar: gelecekteki özellik eklemeleri, mevcut ad oluşturma algoritmasının değişmesine ve geriye dönük uyumluluk sorunlarına yol açabilir. Bu nedenle, bu ayrıntının çıkarılması ve otomatik oluşturulan türün kullanıcı tarafından anlatılamayacağını iddia etmek mantıklıdır.

Neden benzersiz (farklı) türleri kullanmalı?

Bir değerin benzersiz bir türü varsa, optimize eden bir derleyici, garantili doğrulukla tüm kullanım sitelerinde benzersiz bir türü izleyebilir. Sonuç olarak, kullanıcı bu belirli değerin kaynağının derleyici tarafından tam olarak bilindiği yerlerden emin olabilir.

Örnek olarak, derleyicinin gördüğü an:

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

/* ... intervening code */

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

derleyici, kaynağını bile bilmeden gmutlaka kaynaklanması gereken tam bir güvene sahiptir . Bu, çağrının sanallaştırılmasına izin verir . Kullanıcı, buna yol açan benzersiz veri akışını korumaya büyük özen gösterdiğinden, bunu da bilirdi .fggfg

Bu, zorunlu olarak kullanıcının neler yapabileceğini kısıtlar f. Kullanıcı şunları yazma özgürlüğüne sahip değildir:

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

çünkü bu iki farklı türün (yasadışı) birleşmesine yol açacaktır.

Bu sorunu çözmek için, kullanıcı __UniqueFunc042benzersiz olmayan türe yükseltebilir &dyn Fn(),

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

Bu tür silme işlemiyle yapılan takas &dyn Fn(), derleyicinin muhakemesini karmaşıklaştıran kullanımlardır . Verilen:

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

derleyici, işlevden /*expression */mi yoksa başka işlevlerden mi g2kaynaklandığını fve bu kaynağın hangi koşullar altında geçerli olduğunu belirlemek için titizlikle incelemelidir . Birçok durumda, derleyici vazgeçmek olabilir: söyleyebilirdi belki insan g2gerçekten geliyor ftüm durumlarda ancak uzanan yol fiçin g2çok sanal çağrı sonuçlanan deşifre etmek derleyici kıvrık g2kötümser performansı ile.

Bu tür nesneler genel (şablon) işlevlere teslim edildiğinde daha belirgin hale gelir:

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

Biri ararsa h(f)nerede f: __UniqueFunc042, o heşsiz örneğine uzmanlaşmıştır:

h::<__UniqueFunc042>(f);

Bu, derleyicinin hözel argüman için özel kod üretmesini sağlar fve gönderim f, satır içi değilse, büyük olasılıkla statik olacaktır.

Karşı senaryoda, bir kişi h(f)ile arandığında f2: &Fn(), hşu şekilde somutlaştırılır:

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

türdeki tüm işlevler arasında paylaşılır &Fn(). hDerleyici, içeriden , opak bir tür işlevi hakkında çok az şey bilir &Fn()ve bu nedenle, yalnızca fsanal bir gönderimle konservatif olarak çağırabilir . Statik olarak göndermek için, derleyicinin çağrıyı h::<&Fn()>(f)kendi çağrı sitesinde satır içi yapması gerekir ki hbu çok karmaşıksa garanti edilmez .


İsim seçimi ile ilgili ilk kısım meseleyi özlüyor: Gibi bir tipin void(*)(int, double)bir adı olmayabilir, ancak onu yazabilirim. Ben buna isimsiz bir tip derdim, anonim bir tip değil. Ve __namespace1_module1_func1_AnonymousFunction042kesinlikle bu sorunun kapsamına girmeyen isim değiştirme gibi şifreli şeyler diyecektim . Bu soru, bu türleri yararlı bir şekilde ifade edebilen bir tür sözdizimi getirmenin aksine, standart tarafından yazılması imkansız olduğu garanti edilen türlerle ilgilidir.
cmaster - reinstate monica

3

İlk olarak, yakalamasız lambda bir işlev işaretçisine dönüştürülebilir. Bu yüzden bir tür jeneriklik sağlarlar.

Şimdi neden yakalamalı lambdalar işaretçiye dönüştürülemiyor? İşlevin lambda durumuna erişmesi gerektiğinden, bu durumun bir işlev bağımsız değişkeni olarak görünmesi gerekir.


Peki, yakalamalar lambda'nın bir parçası olmalı, değil mi? Tıpkı bir std::function<>.
cmaster

3

Kullanıcı koduyla isim çakışmalarını önlemek için.

Aynı uygulamaya sahip iki lambdanın bile farklı türleri olacaktır. Sorun değil, çünkü bellek düzeni eşit olsa bile nesneler için farklı türlere sahip olabilirim.


Gibi bir tür int (*)(Foo*, int, double), kullanıcı koduyla herhangi bir isim çarpışması riski taşımaz.
cmaster -

Örneğiniz pek iyi bir genelleme yapmıyor. Bir lambda ifadesi sadece sözdizimi iken, özellikle yakalama cümlesiyle bazı yapılara göre değerlendirilecektir. Açıkça adlandırmak, halihazırda mevcut yapıların isim çatışmalarına yol açabilir.
knivil

Yine, bu soru C ++ ile ilgili değil, dil tasarımı ile ilgili. Kesinlikle bir lambda tipinin bir veri yapısı tipinden çok bir fonksiyon gösterici tipine benzer olduğu bir dil tanımlayabilirim. C ++ 'daki işlev işaretçisi sözdizimi ve C'deki dinamik dizi türü sözdizimi bunun mümkün olduğunu kanıtlamaktadır. Ve bu soruyu akla getiriyor, lambdalar neden benzer bir yaklaşım kullanmadı?
cmaster - reinstate monica

1
Değişken currying (yakalama) nedeniyle yapamazsınız. Çalışması için hem bir işleve hem de veriye ihtiyacınız var.
Blindy

@Blindy Oh, evet yapabilirim. Bir lambda'yı, biri yakalama nesnesi ve biri kod için olmak üzere iki işaretçi içeren bir nesne olarak tanımlayabilirim. Böyle bir lambda nesnesinin değerine göre geçişi kolay olacaktır. Veya gerçek lambda koduna atlamadan önce kendi adresini alan yakalama nesnesinin başlangıcında bir kod saplamasıyla hileler çekebilirim. Bu bir lambda işaretçisini tek bir adrese dönüştürecektir. Ancak, PPC platformunun kanıtladığı için bu gereksizdir: PPC'de, bir işlev işaretçisi aslında bir çift işaretleyicidir. Bu yüzden standart C / C ++ ' void(*)(void)a void*ve geri dönüş yapamazsınız .
cmaster - reinstate monica
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.