Навіщо розробляти мову з унікальними анонімними типами?


91

Це те, що мене завжди хвилювало як особливість лямбда-виразів на C ++: Тип лямбда-виразу на C ++ є унікальним та анонімним, я просто не можу записати його. Навіть якщо я створюю дві лямбди, які синтаксично абсолютно однакові, отримані типи визначаються як різні. Наслідком цього є те, що а) лямбди можуть передаватися лише функціям шаблону, які дозволяють передавати час компіляції, невимовний тип разом з об'єктом, і б) що лямбди корисні лише після того, як їх буде видалено std::function<>.

Гаразд, але це саме так, як це робить C ++, я був готовий списати це як просто неприємну властивість цієї мови. Однак я щойно дізнався, що Руст, схоже, робить те саме: кожна функція Руст або лямбда має унікальний, анонімний тип. І зараз мені цікаво: Чому?

Отже, моє запитання полягає в наступному:
яка перевага, з точки зору дизайнера мови, вводити в мову поняття унікального, анонімного типу?


6
як завжди, краще питання, чому б ні.
Stargateur

31
"що лямбди корисні лише після того, як їх буде видалено через std :: function <>" - ні, вони безпосередньо корисні без std::function. Лямбда, передана функції шаблону, може бути викликана безпосередньо без участі std::function. Потім компілятор може вставити лямбда-функцію у функцію шаблону, що покращить ефективність виконання.
Ерлкоеніг

1
Я гадаю, це полегшує реалізацію лямбда-сигналів та полегшує розуміння мови. Якщо ви дозволили точно такий самий лямбда-вираз складати в один і той самий тип, тоді вам знадобляться спеціальні правила для обробки, { int i = 42; auto foo = [&i](){ return i; }; } { int i = 13; auto foo = [&i](){ return i; }; }оскільки змінна, на яку він посилається, відрізняється, хоча по тексту вони однакові. Якщо ви просто кажете, що всі вони унікальні, вам не доведеться турбуватися про те, щоб спробувати це зрозуміти.
NathanOliver

5
але ви також можете назвати тип лямбдасу і зробити все те ж саме з цим. lambdas_type = decltype( my_lambda);
idclev 463035818

3
Але яким повинен бути тип загальної лямбди [](auto) {}? Для початку він повинен мати тип?
Evg

Відповіді:


78

Багато стандартів (особливо С ++) застосовують підхід до мінімізації того, скільки вони вимагають від компіляторів. Чесно кажучи, вони вимагають вже достатньо! Якщо їм не потрібно щось вказувати, щоб це працювало, вони мають тенденцію залишати його реалізацію визначеною.

Якби лямбди не були анонімними, ми повинні були б їх визначити. Це мало би багато сказати про те, як фіксуються змінні. Розглянемо випадок лямбди [=](){...}. Тип повинен був би вказати, які типи насправді були зафіксовані лямбда, що може бути нетривіальним для визначення. Крім того, що робити, якщо компілятор успішно оптимізує змінну? Розглянемо:

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

Оптимізуючий компілятор міг легко визнати, що єдине можливе значення, iяке можна захопити, - 5, і замінити це на auto f = []() { return 5; }. Однак, якщо тип не є анонімним, це може змінити тип або змусити компілятор оптимізувати менше, зберігаючи, iнавіть якщо він йому насправді не потрібен. Це цілий мішок складності та нюансів, який просто не потрібен для того, що лямбди мали робити.

І, не зважаючи на те, що вам насправді потрібен неанонімний тип, ви завжди можете самостійно побудувати клас закриття та працювати з функтором, а не з лямбда-функцією. Таким чином, вони можуть змусити лямбди обробляти 99% випадків і залишити вас кодувати власне рішення в 1%.


Дедулікатор зазначав у коментарях, що я не стосувався не лише унікальності, скільки анонімності. Я менш впевнений у перевагах унікальності, але варто зазначити, що поведінка наведених нижче зрозуміла, якщо типи унікальні (дія буде інстанційована двічі).

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

Якби типи не були унікальними, нам довелося б вказати, яка поведінка повинна відбуватися в цьому випадку. Це може бути складно. Деякі питання, порушені на тему анонімності, також піднімають свою потворну голову в цьому випадку заради унікальності.


Зверніть увагу, що це насправді не про збереження роботи для реалізатора компілятора, а про збереження роботи для супровідника стандартів. Компілятор все ще повинен відповісти на всі вищезазначені запитання для його конкретної реалізації, але вони не вказані в стандарті.
ComicSansMS

2
@ComicSansMS Поєднувати подібні речі при впровадженні компілятора набагато простіше, коли вам не потрібно пристосовувати свою реалізацію до чужого стандарту. Якщо говорити з досвіду, часто суттєво легше перевизначити функціональність спеціаліста, що підтримує стандарти, ніж спробувати знайти мінімальну суму, яку слід вказати, і при цьому отримати бажану функціональність з вашої мови. Як чудове тематичне дослідження, подивіться на те, скільки праці вони витратили, уникаючи надто специфікованого memory_order_consume, водночас роблячи це корисним (для деяких архітектур)
Корт Аммон,

1
Як і всі інші, ви робите вагомі аргументи для анонімних . Але чи справді така гарна ідея змусити його бути також унікальним ?
Дедулікатор

Тут важлива не складність компілятора, а складність згенерованого коду. Суть не в тому, щоб спростити компілятор, а в тому, щоб надати йому достатньо місця для хитання, щоб оптимізувати всі випадки та створити природний код для цільової платформи.
Ян Гудек,

Ви не можете захопити статичну змінну.
Руслан

70

Лямбди - це не просто функції, це функція і держава . Тому як C ++, так і Rust реалізують їх як об'єкт з оператором виклику ( operator()у C ++ - 3 Fn*ознаки в Rust).

В основному, [a] { return a + 1; }в C ++ це щось на зразок

struct __SomeName {
    int a;

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

потім використовуючи приклад, __SomeNameде використовується лямбда.

Перебуваючи в Іржі, || a + 1в Русті перейде на щось подібне

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

Це означає, що більшість лямбд повинні мати різні типи.

Зараз є кілька способів зробити це:

  • З анонімними типами, що реалізують обидві мови. Ще одним наслідком цього є те, що всі лямбди повинні мати різний тип. Але для дизайнерів мов це має очевидну перевагу: лямбди можна просто описати, використовуючи інші вже існуючі простіші частини мови. Вони є просто синтаксичним цукром навколо вже існуючих бітів мови.
  • З деяким спеціальним синтаксисом для іменування типів лямбда: Це, однак, не потрібно, оскільки лямбди вже можна використовувати з шаблонами на C ++ або з узагальненнями та Fn*ознаками в Rust. Жодна з мов ніколи не змушує вас стирати лямбди, щоб використовувати їх ( std::functionу C ++ або Box<Fn*>Rust).

Також зауважте, що обидві мови погоджуються з тим, що тривіальні лямбди, які не фіксують контекст, можуть бути перетворені у покажчики на функції.


Опис складних особливостей мов за допомогою більш простих функцій є досить поширеним явищем. Наприклад, і в C ++, і в Rust є цикли для діапазону, і вони обидва описують їх як синтаксичний цукор для інших функцій.

С ++ визначає

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

як рівнозначний

{

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

        range_declaration = *__begin;
        loop_statement

    }

} 

і Руст визначає

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

як рівнозначний

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

які, хоча вони здаються складнішими для людини, проте простіші для дизайнера мови або компілятора.


15
@ cmaster-reinstatemonica Розгляньте передачу лямбда-сигналу як аргументу порівняння для функції сортування. Ви дійсно хочете накласти тут накладні витрати на віртуальні функції?
Даніель Лангр,

5
@ cmaster-reinstatemonica, оскільки в C ++ нічого не є віртуальним за замовчуванням
Калет,

4
@cmaster - Ви маєте на увазі змусити всіх користувачів лямбда платити за динамічний dipatch, навіть коли він їм не потрібен?
StoryTeller - нежителька Моніка

4
@ cmaster-reinstatemonica Найкраще, що ви отримаєте, - це підписатися на віртуальний. Здогадайтесь, що std::functionце робить
Калет

9
@ cmaster-reinstatemonica будь - який механізм, де ви можете перенаправити функцію, яку потрібно викликати, матиме ситуації із накладними витратами часу виконання. Це не спосіб C ++. Ви std::function
приймаєте рішення

13

(Додавання до відповіді Калета, але занадто довге, щоб вміститися в коментарі.)

Вираз лямбда - це просто синтаксичний цукор для анонімної структури (тип Волдеморта, тому що ви не можете назвати його назву).

Ви можете побачити подібність між анонімною структурою та анонімністю лямбда в цьому фрагменті коду:

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

Якщо це все ще не задовольняє для лямбда-сигналу, це також повинно бути незадовільним для анонімної структури.

Деякі мови дозволяють набирати тип качок, який є трохи гнучкішим, і навіть незважаючи на те, що C ++ має шаблони, які насправді не допомагають створити об'єкт із шаблону, який має поле-член, яке може замінити лямбду безпосередньо, а не використовувати std::functionобгортка.


3
Дякую, це справді проливає трохи світла на міркування щодо того, як лямбди визначаються в C ++ (я повинен пам'ятати термін "тип Волдеморта" :-)). Однак питання залишається: у чому перевага цього в очах дизайнера мов?
cmaster - відновити моніку

1
Ви навіть можете додати int& operator()(){ return x; }до цих структур
Калет

2
@ cmaster-reinstatemonica • Спекулятивно ... решта С ++ поводиться так. Для того, щоб лямбди використовували певний тип качки, який набирає «форми поверхні», це було б чимось дуже відмінним від решти мови. Додавання такого роду засобів у мові для лямбд, мабуть, вважалося б узагальненим для всієї мови, і це могло б бути потенційно величезною зміною. Опускання такої можливості для просто лямбда вписується в сильно набір тексту решти С ++.
Елджей

Технічно це тип Волдеморта auto foo(){ struct DarkLord {} tom_riddle; return tom_riddle; }, оскільки поза fooнічим не можна використовувати ідентифікаторDarkLord
Калет

@ ефективність cmaster-reinstatemonica, альтернативою було б встановити коробку та динамічно відправляти кожну лямбду (розподілити її в купі та стерти її точний тип). Тепер, коли ви зауважуєте, компілятор міг би дедуплікацію анонімних типів лямбда, але ви все одно не змогли б записати їх, і це зажадало б значної роботи для дуже невеликого прибутку, тому шанси насправді не на користь.
Masklinn

10

Навіщо розробляти мову з унікальними анонімними типами?

Тому що бувають випадки, коли імена не мають значення і не корисні, а то й контрпродуктивні. У цьому випадку здатність абстрагувати їх існування корисна, оскільки вона зменшує забруднення імен та вирішує одну з двох важких проблем в галузі комп’ютерних наук (як називати речі). З тієї ж причини тимчасові об'єкти корисні.

лямбда

Унікальність - це не особлива лямбда-штука і навіть не особлива річ для анонімних типів. Це стосується і названих типів у мові. Розгляньте наступне:

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

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

void foo(A);

Зверніть увагу , що я не можу передати Bв foo, навіть якщо класи однакові. Ця ж властивість стосується і неназваних типів.

lambdas можна передавати лише до функцій шаблону, які дозволяють передавати час компіляції, невимовний тип разом з об'єктом ... стирається через std :: function <>.

Існує третій варіант для підмножини лямбд: не захоплюючі лямбди можуть бути перетворені у покажчики на функції.


Зверніть увагу, що якщо обмеження анонімного типу є проблемою для випадку використання, то рішення є простим: замість нього можна використовувати іменований тип. Лямбди не роблять нічого, чого не можна зробити з названим класом.


10

Прийнята відповідь Корта Аммона хороша, але я думаю, що є ще один важливий момент щодо реалізації.

Припустимо, у мене є дві різні одиниці перекладу, "one.cpp" і "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);

Дві перевантаження fooвикористовують один і той самий ідентифікатор ( foo), але мають різні спотворені імена. (В італійському ABI, що використовується в системах POSIX-іш, неправдиві імена є, _Z3foo1Aі, в даному конкретному випадку ,. _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) {}

Компілятор С ++ повинен переконатися, що ім'я зі спотворенням void foo(A1)у "two.cpp" збігається з іменем extern void foo(A2)у "one.cpp", щоб ми могли пов'язати два об'єктні файли разом. Це фізичне значення двох типів, які є "однаковими": це, по суті, ABI-сумісність між окремо скомпільованими об'єктними файлами.

Компілятор C ++ це НЕ потрібно , щоб гарантувати , що B1і B2є «тим же типом.» (Насправді потрібно переконатись, що вони різного типу; але це не так важливо зараз).


Який фізичний механізм робить використання компілятора для того , щоб A1і A2в «той же самий тип»?

Він просто прокопується через typedefs, а потім переглядає повну назву типу. Це тип класу з іменем A. (Ну, ::Aоскільки це в глобальному просторі імен.) Отже, це однаковий тип в обох випадках. Це легко зрозуміти. Що ще важливіше, це легко здійснити . Щоб побачити, чи однакові два типи класів, ви берете їх імена та робите astrcmp . Щоб перетворити тип класу на спотворене ім'я функції, ви вводите кількість символів у його назві, за якими слідують ці символи.

Отже, названі типи легко зруйнувати.

Який фізичний механізм може використання компілятора для того , щоб B1і B2є «тим же типом," в гіпотетичному світі , де C ++ вимагає від них бути тим же типом?

Ну, він не міг використовувати назву типу, оскільки тип не має імені.

Можливо, це могло б якось закодувати текст тіла лямбди. Але це було б якось незручно, оскільки насправді bin "one.cpp" тонко відрізняється від b"two.cpp": "one.cpp" має x+1та "two.cpp" має x + 1. Отже, нам довелося б придумати правило, яке говорить або про те, що ця різниця в пробілах не має значення, або про те, що це робить (зрештою роблячи їх різними типами), або, можливо, це так (можливо, дійсність програми визначається реалізацією , або, можливо, це "неправильно сформовано, діагностика не потрібна"). У будь-якому разі,A

Найпростіший вихід із труднощів - це просто сказати, що кожен лямбда-вираз створює значення унікального типу. Тоді два лямбда-типи, визначені в різних одиницях перекладу, однозначно не є одним типом . У межах однієї одиниці перекладу ми можемо "називати" типи лямбда, просто підраховуючи від початку вихідного коду:

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
} 

Звичайно, ці назви мають значення лише в цій одиниці перекладу. Цей TU $_0завжди відрізняється від деяких інших TU $_0, хоча цей TU struct Aзавжди має той самий тип, що і деякі інші TU struct A.

До речі, зауважте, що у нашої ідеї "кодування тексту лямбда" була ще одна тонка проблема: лямбди $_2і $_3складаються з абсолютно однакового тексту , але їх явно не слід вважати однотипними !


До речі, С ++ вимагає, щоб компілятор знав, як маніпулювати текстом довільного виразу С ++ , як у

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

Але C ++ ще (не) вимагає, щоб компілятор знав, як маніпулювати довільним оператором C ++ . decltype([](){ ...arbitrary statements... })все ще погано сформований навіть у C ++ 20.


Також зауважте, що легко надати локальний псевдонім неназваному типу, використовуючи typedef/ using. Я відчуваю, що ваше запитання могло виникнути в результаті спроби зробити щось, що можна було б вирішити таким чином.

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

ВИДАЛО ДОДАТИ: Читаючи деякі ваші коментарі до інших відповідей, здається, ви задаєтеся питанням, чому

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

Це тому, що беззахоплені лямбди можна конструювати за замовчуванням. (У С ++ лише станом на С ++ 20, але це завжди було концептуально істинним.)

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

Якби ви спробували default_construct_and_call<decltype(&add1)>, tце був би вказівник на функцію, який ініціалізувався за замовчуванням, і ви, мабуть, segfault. Це, начебто, не корисно.


" Насправді, потрібно переконатися, що вони різного типу; але це не настільки важливо зараз ". Цікаво, чи є вагома причина для примусу до унікальності, якщо це рівнозначно визначено.
Дедулікатор

Особисто я думаю, що повністю визначена поведінка (майже?) Завжди краща за невизначену поведінку. "Чи рівні ці два покажчики на функції? Ну, лише якщо ці два екземпляри шаблону є однаковою функцією, що відповідає дійсності, лише якщо ці два лямбда-типи однакові, що відповідає дійсності, лише якщо компілятор вирішив їх об'єднати." Ікі! (Але зауважте, що ми маємо точно аналогічну ситуацію зі злиттям буквальних рядків, і ніхто не бентежить цю ситуацію. Тож я сумніваюся, що було б катастрофічно дозволити компілятору об’єднувати однакові типи.)
Quuxplusone

Ну, чи однакові дві еквівалентні функції (заборона як би) теж є приємним питанням. Мова в стандарті не зовсім очевидна для вільних та / або статичних функцій. Але це поза сферою застосування тут.
Дедулікатор

Без сумніву, цього місяця в списку розсилки LLVM обговорювались функції злиття. Кодеген Clang призведе до того, що функції з абсолютно порожніми тілами будуть злиті майже "випадково": godbolt.org/z/obT55b Це технічно не відповідає, і я думаю, що вони, ймовірно, виправлять LLVM, щоб припинити це робити. Так, погоджене, об’єднання адрес функцій - це теж річ.
Quuxplusone

Цей приклад має інші проблеми, а саме відсутність оператора return. Хіба вони самі не роблять код невідповідним? Крім того, я буду шукати обговорення, але чи показали вони або припустили, що об’єднання еквівалентних функцій не відповідає стандарту, їх документованій поведінці, gcc або просто те, що деякі покладаються на те, що цього не відбувається?
Дедулікатор

9

Лямбди С ++ потрібні різні типи для різних операцій, оскільки С ++ зв'язується статично. Вони можуть створюватися лише для копіювання / переміщення, тому здебільшого вам не потрібно називати їх тип. Але це все дещо у деталі реалізації.

Я не впевнений, чи мають лямбди C # тип, оскільки вони є "анонімними виразами функції", і вони відразу перетворюються на сумісний тип делегатів або тип дерева виразів. Якщо це так, це, мабуть, невимовний тип.

С ++ також має анонімні структури, де кожне визначення веде до унікального типу. Тут назва не є невимовною, вона просто не існує, що стосується стандарту.

C # має анонімні типи даних , яким він обережно забороняє виходити з області дії, яку вони визначили. Реалізація дає унікальне, невимовне ім’я для них.

Наявність анонімного типу сигналізує програмісту про те, що їм не слід тикатися всередині їх реалізації.

Окрім:

Ви можете назвати тип лямбда-типу.

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

Якщо у вас немає захоплень, ви можете використовувати тип покажчика функції

void (*pfoo)() = foo;

1
Перший приклад коду все одно не дозволяє наступний Foo_t = []{};, лише Foo_t = fooі нічого іншого.
cmaster - відновити моніку

1
@ cmaster-reinstatemonica це тому, що тип не може бути побудований за замовчуванням, а не через анонімність. Я припускаю, що це стільки ж стосується уникнення ще більшого набору кутових футлярів, про які ви повинні пам’ятати, як і будь-яка технічна причина.
Калет,

6

Навіщо використовувати анонімні типи?

Для типів, які автоматично генеруються компілятором, вибір полягає в тому, щоб (1) виконати запит користувача щодо імені типу, або (2) дозволити компілятору вибрати його самостійно.

  1. У першому випадку користувач повинен явно вказати ім'я кожного разу, коли з'являється така конструкція (C ++ / Rust: щоразу, коли визначається лямбда; Rust: коли визначається функція). Користувач надає цю нудну деталь кожного разу, і в більшості випадків це ім’я більше ніколи не згадується. Таким чином, має сенс дозволити компілятору автоматично визначити його назву та використовувати існуючі функції, такі як decltypeабо умовивід типу, щоб посилатися на тип у тих кількох місцях, де це потрібно.

  2. В останньому випадку компілятору потрібно вибрати унікальне ім'я для типу, яке, ймовірно, було б незрозумілим, нечитабельним іменем, таким як __namespace1_module1_func1_AnonymousFunction042. Мовний дизайнер міг би точно вказати, як ця назва побудована в чудових і делікатних деталях, але це без потреби розкриває користувачеві деталь, на яку не міг би покластись жоден розумний користувач, оскільки назва, без сумніву, крихка перед навіть незначними рефакторами. Це також непотрібно стримує еволюцію мови: майбутні доповнення функцій можуть призвести до зміни існуючого алгоритму генерації імен, що призведе до проблем зі зворотною сумісністю. Таким чином, має сенс просто опустити цю деталь і стверджувати, що автоматично згенерований тип не піддається користувачеві.

Навіщо використовувати унікальні (чіткі) типи?

Якщо значення має унікальний тип, то оптимізуючий компілятор може відстежувати унікальний тип на всіх його сайтах використання із гарантованою достовірністю. Як наслідок, користувач може тоді бути певним із місць, де походження цього конкретного значення повністю відоме компілятору.

Як приклад, момент, коли компілятор побачить:

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

/* ... intervening code */

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

компілятор має повну впевненість, яка gобов'язково повинна походити з f, навіть не знаючи про походження g. Це дозволило gб девертуалізувати дзвінок. Користувач теж це знав би, оскільки користувач подбав про те, щоб зберегти унікальний тип fчерез потік даних, що призвів доg .

Це обов’язково обмежує те, з чим користувач може робити f. Користувач не може писати:

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

оскільки це призвело б до (незаконного) об'єднання двох різних типів.

Щоб обійти це, користувач міг оновити __UniqueFunc042до не унікального типу &dyn Fn(),

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

Компроміс, зроблений стиранням цього типу, полягає в тому, що використання &dyn Fn()ускладнює міркування компілятора. Дано:

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

компілятор повинен ретельно вивчити, /*expression */щоб визначити, g2походить від fякоїсь іншої функції (функцій) та умови, за яких це походження походить . За багатьох обставин компілятор може відмовитись: можливо, людина міг би сказати, що g2насправді походить fу будь-яких ситуаціях, але шлях від fдо g2був занадто заплутаним, щоб компілятор розшифрував, що призвело до віртуального дзвінка до g2з песимістичною продуктивністю.

Це стає більш очевидним, коли такі об'єкти доставляються до загальних (шаблонних) функцій:

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

Якщо хтось дзвонить h(f)куди f: __UniqueFunc042, то hспеціалізується на унікальному екземплярі:

h::<__UniqueFunc042>(f);

Це дає змогу компілятору генерувати спеціалізований код для h, спеціально розроблений для конкретного аргументу f, і відправка до fцілком ймовірно статична, якщо не вбудована.

У протилежному сценарії, коли хтось телефонує за h(f)допомогою f2: &Fn(), hінстанціюється як

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

який спільно використовується між усіма функціями типу &Fn(). Зсередини hкомпілятор дуже мало знає про непрозору функцію типу, &Fn()тому може лише консервативно телефонувати fза допомогою віртуальної диспетчеризації. Щоб статично відправити, компілятору доведеться вбудувати виклик до місця h::<&Fn()>(f)виклику, що не гарантується, якщо hвоно занадто складне.


Перша частина про вибір імен не відповідає суті: такий тип void(*)(int, double)може не мати імені, але я можу записати його. Я б назвав це безіменним, а не анонімним типом. І я б назвав загадкові речі, такі як __namespace1_module1_func1_AnonymousFunction042перекручування імен, що точно не входить у сферу цього питання. Це питання стосується типів, які гарантовано стандартом неможливо записати, на відміну від введення синтаксису типу, який може виражати ці типи корисним чином.
cmaster - відновити моніку

3

По-перше, лямбда без захоплення конвертується у покажчик на функцію. Отже, вони забезпечують певну форму загальності.

Тепер, чому лямбди з захопленням не конвертовані в покажчик? Оскільки функція повинна отримувати доступ до стану лямбда, тому цей стан повинен відображатися як аргумент функції.


Ну, захоплення повинно стати частиною самої лямбди, ні? Так само, як вони інкапсульовані в std::function<>.
cmaster - відновити моніку

3

Щоб уникнути зіткнень імен з кодом користувача.

Навіть дві лямбди з однаковим виконанням матимуть різні типи. Що нормально, оскільки я також можу мати різні типи об’єктів, навіть якщо їх розміщення в пам’яті рівне.


Тип типу int (*)(Foo*, int, double)не ризикує зіткнення імені з кодом користувача.
cmaster - відновити моніку

Ваш приклад не дуже добре узагальнює. Хоча лямбда-вираз - це лише синтаксис, він буде оцінений до певної структури, особливо з пропозицією захоплення. Явне його називання може призвести до зіткнень імен уже існуючих структур.
knivil

Знову ж таки, це питання стосується мовного дизайну, а не C ++. Я точно можу визначити мову, де лямбда-тип більше схожий на тип покажчика на функцію, ніж на тип структури даних. Синтаксис покажчика функції на C ++ та синтаксис типу динамічного масиву на C доводять, що це можливо. І тут виникає питання, чому лямбди не застосовували подібний підхід?
cmaster - відновити моніку

1
Ні, ви не можете, через змінне каррінг (захоплення). Вам потрібна як функція, так і дані, щоб вона працювала.
Сліпий

@Blindy О, так, я можу. Я міг би визначити лямбду як об’єкт, що містить два покажчики, один для об’єкта захоплення, а другий для коду. Такий лямбда-об'єкт було б легко передати за значенням. Або я міг би витягнути хитрощі із заглушкою коду на початку об’єкта захоплення, який бере свою адресу перед тим, як перейти до власне лямбда-коду. Це перетворило б лямбда-вказівник в одну адресу. Але це непотрібно, як довела платформа PPC: на PPC покажчик функції насправді є парою покажчиків. Ось чому ви не можете робити трансляції void(*)(void)до void*і назад у стандартному C / C ++.
cmaster
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.