Зачем создавать язык с уникальными анонимными типами?

90

Это то, что меня всегда беспокоило как особенность лямбда-выражений C ++: тип лямбда-выражения C ++ уникален и анонимен, я просто не могу его записать. Даже если я создам две лямбда-выражения, которые синтаксически совершенно одинаковы, результирующие типы определены как разные. Следствием этого является то, что а) лямбда-выражения могут быть переданы только шаблонным функциям, которые позволяют передавать вместе с объектом время компиляции, невыразимый тип, и б) лямбда-выражения полезны только после того, как они были стерты через тип std::function<>.

Хорошо, но именно так это делает C ++, я был готов списать это на утомительную особенность этого языка. Однако я только что узнал, что Rust, похоже, делает то же самое: каждая функция или лямбда Rust имеет уникальный анонимный тип. А теперь мне интересно: почему?

Итак, у меня такой вопрос: в
чем преимущество с точки зрения разработчика языка введение в язык концепции уникального анонимного типа?

cmaster - восстановить монику
источник
6
как всегда, лучший вопрос - почему бы и нет.
Stargateur
31
"эти лямбды полезны только после того, как они были стерты с помощью std :: function <>" - нет, они полезны напрямую без std::function. Лямбда, переданная в шаблонную функцию, может быть вызвана напрямую без участия std::function. Затем компилятор может встроить лямбда в функцию шаблона, что повысит эффективность выполнения.
Erlkoenig
1
Полагаю, это упрощает реализацию лямбда-выражения и упрощает понимание языка. Если вы разрешили преобразование одного и того же лямбда-выражения в один и тот же тип, тогда вам потребуются специальные правила для обработки, { int i = 42; auto foo = [&i](){ return i; }; } { int i = 13; auto foo = [&i](){ return i; }; }поскольку переменная, на которую оно ссылается, отличается, даже если текстуально они одинаковы. Если вы просто скажете, что все они уникальны, вам не нужно беспокоиться о попытках понять это.
Натан Оливер
5
но вы также можете дать имя лямбда-типу и проделать то же самое с ним. lambdas_type = decltype( my_lambda);
idclev 463035818
3
Но каким должен быть тип общей лямбды [](auto) {}? Следует ли для начала иметь тип?
Evg

Ответы:

78

Многие стандарты (особенно C ++) сводятся к минимуму того, сколько они требуют от компиляторов. Честно говоря, они уже достаточно требуют! Если им не нужно указывать что-то, чтобы это работало, они, как правило, оставляют реализацию определенной.

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

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где используется лямбда.

Находясь в Rust, || a + 1в Rust будет десахарироваться до чего-то вроде

{
    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 имеют циклы range-for, и оба описывают их как синтаксический сахар для других функций.

C ++ определяет

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

    }

} 

а Rust определяет

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

которые кажутся более сложными для человека, но более простыми для разработчика языков или компилятора.

Mcarton
источник
15
@ cmaster-reinstatemonica Рассмотрите возможность передачи лямбда-выражения в качестве аргумента компаратора для функции сортировки. Вы действительно хотите наложить здесь накладные расходы на вызовы виртуальных функций?
Дэниел Лангр,
5
@ cmaster-reinstatemonica, потому что в C ++ по умолчанию нет ничего виртуального
Калет
4
@cmaster - Вы имеете в виду заставить всех пользователей лямбда платить за динамический дипатч, даже если он им не нужен?
StoryTeller - Unslander Моника
4
@ cmaster-reinstatemonica Лучшее, что вы получите, - это подпишитесь на виртуальный. Угадайте, что, std::functionделает это
Калет
9
@ cmaster-reinstatemonica любой механизм, в котором вы можете перенаправить вызываемую функцию, будет иметь ситуации с накладными расходами во время выполнения. Это не способ С ++. Вы 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 • Спекулятивно ... остальная часть C ++ ведет себя именно так. Заставить лямбды использовать некую «форму поверхности» утиного набора было бы чем-то совершенно отличным от остального языка. Добавление такого рода возможностей в язык для лямбда-выражений, вероятно, будет рассматриваться как обобщенное для всего языка, и это будет потенциально огромным критическим изменением. Отсутствие такой возможности только для лямбда-выражений согласуется со строгой типизацией остальной части C ++.
Эльджай
Технически это был бы тип Волан-де-Морта auto foo(){ struct DarkLord {} tom_riddle; return tom_riddle; }, потому что fooничто не может использовать идентификатор DarkLord
извне
@ cmaster-reinstatemonica, альтернативой будет упаковка и динамическая отправка каждой лямбды (разместить ее в куче и стереть ее точный тип). Теперь, как вы заметили, компилятор может дедуплицировать анонимные типы лямбд, но вы все равно не сможете их записать, и это потребует значительной работы с очень небольшим выигрышем, так что шансы на самом деле не в пользу.
Масклинн
10

Зачем создавать язык с уникальными анонимными типами?

Потому что есть случаи, когда имена неуместны, бесполезны или даже контрпродуктивны. В этом случае способность абстрагироваться от их существования полезна, потому что она уменьшает загрязнение имен и решает одну из двух сложных проблем в информатике (как называть вещи). По той же причине полезны временные объекты.

лямбда

Уникальность - это не особая лямбда-функция или даже особенность анонимных типов. Это также применимо к именованным типам в языке. Учтите следующее:

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

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

void foo(A);

Обратите внимание, что я не могу перейти Bвfoo , даже если классы одинаковы. Это же свойство применимо к безымянным типам.

лямбда-выражения могут быть переданы только шаблонным функциям, которые позволяют передавать во время компиляции невыразимый тип вместе с объектом ... стираемым с помощью std :: function <>.

Есть третий вариант для подмножества лямбда-выражений: лямбда-выражения без захвата могут быть преобразованы в указатели на функции.


Обратите внимание, что если ограничения анонимного типа являются проблемой для варианта использования, решение простое: вместо этого можно использовать именованный тип. Лямбды не делают ничего, чего нельзя было бы сделать с именованным классом.

eerorika
источник
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), но имеют разные искаженные имена. (В Itanium 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) {}

Компилятор C ++ должен гарантировать, что искаженное имя void foo(A1)в "two.cpp" совпадает с искаженным именем extern void foo(A2)в "one.cpp", чтобы мы могли связать два объектных файла вместе. Это физический смысл того, что два типа являются «одним и тем же типом»: по сути, речь идет о совместимости с ABI между отдельно скомпилированными объектными файлами.

Компилятор C ++ не обязан гарантировать, что B1и B2являются «одного типа». (Фактически, необходимо убедиться, что это разные типы, но сейчас это не так важно.)


Какой физический механизм использует компилятор, чтобы гарантировать, что A1и A2являются «одного типа»?

Он просто копается в определениях типов, а затем смотрит на полное имя типа. Это тип класса с именем A. (Ну, ::Aпоскольку он находится в глобальном пространстве имен.) Таким образом, это один и тот же тип в обоих случаях. Это легко понять. Что еще более важно, это легко реализовать . Чтобы увидеть, являются ли два типа классов одним и тем же типом, вы берете их имена и выполняетеstrcmp . Чтобы преобразовать тип класса в искаженное имя функции, вы пишете количество символов в его имени, за которым следуют эти символы.

Итак, именованные типы легко подделать.

Какой физический механизм мог бы использовать компилятор, чтобы гарантировать, что B1и B2являются «одного типа» в гипотетическом мире, где C ++ требует, чтобы они были одного типа?

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

Возможно, он мог как-то закодировать текст тела лямбды. Но это было бы немного неудобно, потому что на самом деле bin "one.cpp" немного отличается от bin "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состоит из точно такого же текста , но они не должны четко рассматриваться тем же типа!


Между прочим, C ++ требует, чтобы компилятор знал, как искажать текст произвольного выражения C ++ , как в

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

Это потому, что лямбды без захвата могут быть сконструированы по умолчанию. (В C ++ только с C ++ 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
источник
« На самом деле, необходимо убедиться, что они разных типов, но сейчас это не так важно». Интересно, есть ли веская причина для принудительного определения уникальности, если оно равно определено.
Дедупликатор
Лично я считаю, что полностью определенное поведение (почти?) Всегда лучше неопределенного. «Равны ли эти два указателя на функции? Ну, только если эти два экземпляра шаблона являются одной и той же функцией, что верно, только если эти два лямбда-типа относятся к одному типу, что верно, только если компилятор решил их объединить». Ики! (Но обратите внимание, что у нас точно такая же ситуация со слиянием строковых литералов, и эта ситуация никого не беспокоит . Поэтому я сомневаюсь, что было бы катастрофой разрешить компилятору объединять идентичные типы.)
Quuxplusone
Ну, могут ли быть идентичны две эквивалентные функции (за исключением «как если бы») - тоже хороший вопрос. Язык в стандарте не совсем очевиден для бесплатных и / или статических функций. Но здесь это выходит за рамки.
Дедупликатор
По счастливой случайности, в этом же месяце в списке рассылки LLVM обсуждалось слияние функций. Генерация кода Clang приведет к тому, что функции с полностью пустыми телами будут объединены почти «случайно»:Генерация кода godbolt.org/z/obT55b Это технически не соответствует требованиям, и я думаю, что они, вероятно, исправят LLVM, чтобы прекратить это делать. Но да, согласен, слияние адресов функций тоже имеет смысл.
Quuxplusone
В этом примере есть другие проблемы, а именно отсутствует оператор возврата. Разве они одни уже не делают код несоответствующим? Кроме того, я поищу обсуждение, но показывали ли они или предполагали, что слияние эквивалентных функций не соответствует стандарту, их задокументированному поведению, gcc, или просто некоторые полагаются на то, что этого не происходит?
Дедупликатор
9

Лямбда-выражения C ++ нуждаются в разных типах для различных операций, поскольку C ++ связывает статически. Их можно только копировать / перемещать, поэтому в большинстве случаев вам не нужно указывать их тип. Но это все детали реализации.

Я не уверен, есть ли у лямбда-выражений C # тип, поскольку они являются «выражениями анонимных функций» и сразу же преобразуются в совместимый тип делегата или тип дерева выражений. Если да, вероятно, это непроизносимый тип.

В 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он слишком сложен.

Rufflewind
источник
Первая часть о выборе имен упускает из виду главное: у такого типа, как, void(*)(int, double)может не быть имени, но я могу его записать. Я бы назвал это безымянным типом, а не анонимным типом. И я бы назвал такие загадочные вещи, как __namespace1_module1_func1_AnonymousFunction042искажение имен, что определенно выходит за рамки этого вопроса. Этот вопрос касается типов, которые, как гарантируется стандартом, невозможно записать, в отличие от введения синтаксиса типов, который может выражать эти типы полезным способом.
cmaster - восстановить Монику
3

Во-первых, лямбда-выражения без захвата можно преобразовать в указатель на функцию. Таким образом, они обеспечивают некоторую универсальность.

Теперь почему лямбды с захватом не преобразовываются в указатель? Поскольку функция должна иметь доступ к состоянию лямбда-выражения, это состояние должно отображаться как аргумент функции.

Олив
источник
Что ж, захваты должны стать частью самой лямбды, не так ли? Так же, как они заключены в std::function<>.
cmaster - восстановить Монику
3

Чтобы избежать конфликта имен с кодом пользователя.

Даже две лямбды с одинаковой реализацией будут иметь разные типы. Это нормально, потому что у меня тоже могут быть разные типы объектов, даже если их расположение в памяти одинаково.

Книвил
источник
Тип, подобный int (*)(Foo*, int, double), не подвергается риску столкновения имени с кодом пользователя.
cmaster - восстановить Монику
Ваш пример не очень хорошо обобщает. Хотя лямбда-выражение - это только синтаксис, оно будет оценивать некоторую структуру, особенно с предложением захвата. Его явное присвоение имени может привести к конфликту имен уже существующих структур.
Книвил
Опять же, это вопрос о языковом дизайне, а не о C ++. Я с уверенностью могу определить язык, в котором лямбда-тип больше похож на тип указателя на функцию, чем на тип структуры данных. Синтаксис указателя на функцию в C ++ и синтаксис типа динамического массива в C доказывают, что это возможно. Возникает вопрос, почему лямбды не использовали подобный подход?
cmaster - восстановить Монику
1
Нет, нельзя, из-за каррирования (захвата) переменных. Для работы вам нужны как функция, так и данные.
Blindy
@ Блинди О, да, я могу. Я мог бы определить лямбда как объект, содержащий два указателя: один для объекта захвата, а другой - для кода. Такой лямбда-объект было бы легко передать по значению. Или я мог бы потянуть трюки с помощью куска кода в начале объекта захвата, который принимает собственный адрес перед переходом к фактическому лямбда-коду. Это превратит лямбда-указатель в один адрес. Но в этом нет необходимости, поскольку платформа PPC доказала: на PPC указатель функции на самом деле является парой указателей. Вот почему вы не можете выполнять приведение void(*)(void)к void*стандартному C / C ++ и обратно.
cmaster - восстановить Монику