ideas icon indicating copy to clipboard operation
ideas copied to clipboard

Упрощение синтаксиса захвата лямбда функций

Open AI-Decay opened this issue 2 years ago • 12 comments

Описание идеи

В С++ мы имеем:

int variable = 0; 
auto a = [variable]() {} - Захват по значению
auto a = [&variable]() {} - Захват по ссылке 
auto a = [variable = std::move(variable)]() {} - Но для захвата с перемещением 
необходимо использовать синтаксис с переименованием 

Предлагаю для простоты и однородности использовать подобный синтаксис: auto a = [&&variable]() {} // тождественно равно auto a = [variable = std::move(variable)]() {}

Пример использования:


int main() {

    auto enableButtons = [](bool enabled) {

    };

    auto callback = [&&enableButtons]() {
        enableButtons(true);
    };

    return 0;
}

AI-Decay avatar Aug 12 '21 23:08 AI-Decay

Во-первых, это попросту использование обобщенного синтаксиса:

const auto [var = GetAwesomeValue()] { /* ... */ }

Во-вторых, в таком случае теряется некоторая смысловая нагрузка: при захвате по lvalue-ссылке в лямбду кладется ссылка на имеющийся объект/переменную. При этом исходный объект остается без изменений. Синтаксис в некотором смысле эту связь поддерживает.

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

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

std::unique_ptr<GreatType> object = ...;

std::thread t([&&object] { /* ... */ });

// ...

t.join();

возникает такое ощущение, что указатель до сих пор указывает на нечто валидное. На деле это не так. А вот в коде ниже прямо-таки явно прописано, что наш юник-поинтер мы мувнули и он будет уже чистеньким:

std::unique_ptr<GreatType> object = ...;

std::thread t([object = std::move(object)] { /* ... */ });

// ...

t.join();

В-третьих, предлагаемый подход не совсем подходит той семантике перемещения, которая в С++ существует. Перемещение в С++ явное - через вызов std::move, а этот кейс - вопиющее исключение. В целом это пересекается частично со сказанным выше.

В-четвертых, надо будет дорабатывать еще и статические анализаторы, чтобы они видели такие вот неявные мувы.

В-пятых, получается, что можно будет одно и то же сделать по-разному. Зачем еще одно такое место?

GeorgyFirsov avatar Aug 13 '21 19:08 GeorgyFirsov

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

std::unique_ptr<GreatType> object = ...;

std::thread t([&&object] { /* ... */ });

// ...

t.join();

возникает такое ощущение, что указатель до сих пор указывает на нечто валидное.

Как может возникать такое ощущение если вы явно видите && вместо std::move()?

[](auto &&) { /* ... */ } вы тут явно видите std::move? А он может быть, а может не быть.

Gargony avatar Aug 16 '21 04:08 Gargony

вы тут явно видите std::move?

std::vector<AwesomeType> obj1 = ..., obj2 = ...;

const auto foo = [](auto &&) { /* ... */ };

foo(std::move(obj1)); // Вот тут прекрасно вижу, перемещение есть
foo(obj2);            // А тут - не вижу. Но и никакого перемещения тут нет

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

Вот эту же самую семантику хочется сохранить и для захвата: если я вижу в списке захвата мув, то значит объект перемещен в лямбду, а если не вижу, то не перемещен.

GeorgyFirsov avatar Aug 16 '21 07:08 GeorgyFirsov

foo(std::move(obj1)); // Вот тут прекрасно вижу, перемещение есть
foo(obj2);            // А тут - не вижу. Но и никакого перемещения тут нет

так речь идёт об упрощении данного синтаксиса, что бы не писать длинный std:move...

foo(&&obj1); 
foo(obj2);

Gargony avatar Aug 16 '21 10:08 Gargony

так речь идёт об упрощении данного синтаксиса, что бы не писать длинный std:move...

Ну а я привожу аргументы против такого "упрощения". Кроме того, это не упрощение синтаксиса, а введение нового (и это большая разница).

Более того, как раз вот так foo(&&obj) никто писать не предлагает (!!!)

Речь идёт об отдельном кейсе - списке захвата лямбды. То есть этот кейс теперь будет выделяться и причем не в лучшую сторону. В дополнение, нововведение усложнит и синтаксический анализ языка за счёт того, что вводится ещё один контекст для конструкции &&.

К тому же, разве std::move такой уж длинный? Не настолько, как мне кажется, чтобы вводить отдельный синтаксис. Да и не понимаю этого странного стремления укорачивать код за счет его читаемости или удобства поддержки.

GeorgyFirsov avatar Aug 16 '21 10:08 GeorgyFirsov

Ну а я привожу аргументы против такого "упрощения". Кроме того, это не упрощение синтаксиса, а введение нового (и это большая разница).

А где граница между упрощение синтаксиса и введение нового?

Более того, как раз вот так foo(&&obj) никто писать не предлагает (!!!)

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

Речь идёт об отдельном кейсе - списке захвата лямбды. То есть этот кейс теперь будет выделяться и причем не в лучшую сторону.

Это субективно.

В дополнение, нововведение усложнит и синтаксический анализ языка за счёт того, что вводится ещё один контекст для конструкции &&.

Вы случайно не с компании которая разрабатывает анализатор? ;)

К тому же, разве std::move такой уж длинный?

По мне так очень длинный.

Да и не понимаю этого странного стремления укорачивать код за счет его читаемости или удобства поддержки.

1+2-3*4/6 1 плюс 2 минус 3 умножить 4 делить 6 Что удобнее?

Gargony avatar Aug 16 '21 11:08 Gargony

А где граница между упрощение синтаксиса и введение нового?

Вот CTAD, например, это упрощение:

std::vector<int> v1 = { 1, 2, 3, 4 };
std::vector v2 = { 1, 2, 3, 4 };

Абстрактный синтаксис не поменялся, это все еще одно и то же: T object = { arg1, arg2, ... };

Рассматриваемый же кейс - введение того, чего в принципе ранее не было, введение новых продукций в грамматику.

1+2-3*4/6 1 плюс 2 минус 3 умножить 4 делить 6 Что удобнее?

Тут операторы очевидны. А вот [&&obj] { ... } - нет. Язык не следует превращать в набор тонны разных операторов и конструкций из символов, которые не имеют очевидного толкования. Вот как раз наличие таких причуд только усложняет язык (за примером можно сбегать и поглядеть на Scala или Haskell, которые грешны разного рода символическими конструкциями). Так что сравнение несколько некорректно.

Это субективно.

Может быть, конечно, и субъективно, но тем не менее у меня созрел такой вот еще пример на тему того, чем плохо такое выделение кейса со списком захвата:

std::string &&s = "Hello"; // Специально приклею && к s

И вот почти похожий кейс:

std::string s = "Hello";
const auto foo = [&&s] { ... }

В первой то ситуации никакого перемещения нет, создали rvalue-ссылку просто на временную строку, лайфтайм которой продлен.

И второй кейс уж очень сильно похож на первый случай, но тут мы перемещаем уже строчку внутрь лямбды, а не создаем rvalue-ссылку. А синтаксис то прямо как у т.н. declarator: declarator ::= && [attr] declarator

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

T&& obj = another_obj; // Сейчас не работает (cannot bind to lvalue)
                       // Да и смысл тогда этой записи изменится
T obj = &&another_obj; // Выглядит так себе, так как неочевидный синтаксис для перемещения
                       // std::move как раз повышает читаемость и поддерживаемость такого кода 

GeorgyFirsov avatar Aug 16 '21 14:08 GeorgyFirsov

Абстрактный синтаксис не поменялся, это все еще одно и то же: T object = { arg1, arg2, ... };

Но где-то в коде есть ещё и сам CTAD для vector?

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

Как вы считает почему C вместо begin и end имеет {} и так популярен? Как вам Rust?

Может быть, конечно, и субъективно, но тем не менее у меня созрел такой вот еще пример на тему того, чем плохо такое выделение кейса со списком захвата:

std::string &&s = "Hello"; // Специально приклею && к s

И вот почти похожий кейс:

std::string s = "Hello";
const auto foo = [&&s] { ... }

В первой то ситуации никакого перемещения нет, создали rvalue-ссылку просто на временную строку, лайфтайм которой продлен.

И второй кейс уж очень сильно похож на первый случай, но тут мы перемещаем уже строчку внутрь лямбды, а не создаем rvalue-ссылку. А синтаксис то прямо как у т.н. declarator: declarator ::= && [attr] declarator

Не совсем понял что вы пытались объяснить, но эти два случая совсем не похоже. Если первый прировнять ко второму, то он выглядел бы:

std::string s = "Hello";
std::string&&s1 = s;

что бы работало сейчас:

std::string s = "Hello";
std::string&&s1 = std::move(s);

и новый "сахар":

std::string s = "Hello";
std::string&&s1 = &&s;

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

Да нет же, не только для лямбд, а для всего. Но в лямбдах это очень часто необходимо. Да и лямбда с N захватом через std::move выглядеть очень страшно.

  • для шаблонов это будет std::forward

Gargony avatar Aug 17 '21 03:08 Gargony

Если оба варианта в чём-то не очень

auto a = [variable = std::move(variable)]() {} // многословный и предлагается упростить
auto a = [&&variable]() {} // не удовлетворяет духу C++ и может запутать

То почему бы не рассмотреть третий, компромиссный?

auto a = [std::move(variable)]() {}

Izaron avatar Sep 05 '21 14:09 Izaron

Если оба варианта в чём-то не очень То почему бы не рассмотреть третий, компромиссный?

что делать в случае auto a = [std::move(getMyVariable())] {}?

tomilov avatar Sep 05 '21 15:09 tomilov

Если оба варианта в чём-то не очень То почему бы не рассмотреть третий, компромиссный?

что делать в случае auto a = [std::move(getMyVariable())] {}?

По постановке идеи похоже, что [&&x]/[std::move(x)] должны позволяться только для x из очень ограниченного множества; а именно для таких, что запись [x = std::move(x)] была бы легальной (именно внутри capture list).

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

struct { std::string s } dummy;
auto a = [std::move(dummy.s)]() { ... };

(Но у автора идеи могут быть другие мнения про множество упростимых выражений)

Izaron avatar Sep 05 '21 15:09 Izaron

Во-первых, это попросту использование обобщенного синтаксиса:

const auto [var = GetAwesomeValue()] { /* ... */ }

Этот синтаксис очень многословен, к тому же со стороны с++ выглядит контринтуитивно. К примеру эта запись уб, но нечто подобное в списке захвата считается корректным int a = a;

В-третьих, предлагаемый подход не совсем подходит той семантике перемещения, которая в С++ существует. Перемещение в С++ явное - через вызов std::move, а этот кейс - вопиющее исключение. В целом это пересекается частично со сказанным выше.

А это проблема? Если ввести новвое правило, что && в лямбде это move, то всё становится довольно явно

В-четвертых, надо будет дорабатывать еще и статические анализаторы, чтобы они видели такие вот неявные мувы.

Не занимаюсь разработкой статических анализаторов, но задача выглядит тривиальной

В-пятых, получается, что можно будет одно и то же сделать по-разному. Зачем еще одно такое место?

В С++ куча способов сделать одно и то же по разному, в даном случае это сахар, потому что нынешние ляибды выглядят громоздко

AI-Decay avatar Oct 03 '21 23:10 AI-Decay