ideas
ideas copied to clipboard
Упрощение синтаксиса захвата лямбда функций
Описание идеи
В С++ мы имеем:
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;
}
Во-первых, это попросту использование обобщенного синтаксиса:
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
, а этот кейс - вопиющее исключение. В целом это пересекается частично со сказанным выше.
В-четвертых, надо будет дорабатывать еще и статические анализаторы, чтобы они видели такие вот неявные мувы.
В-пятых, получается, что можно будет одно и то же сделать по-разному. Зачем еще одно такое место?
Предлагаемый же синтаксис не вполне явно разрывает эту связь. Вот при чтении (особенно незнакомого) кода по типу:
std::unique_ptr<GreatType> object = ...; std::thread t([&&object] { /* ... */ }); // ... t.join();
возникает такое ощущение, что указатель до сих пор указывает на нечто валидное.
Как может возникать такое ощущение если вы явно видите &&
вместо std::move()?
[](auto &&) { /* ... */ }
вы тут явно видите std::move? А он может быть, а может не быть.
вы тут явно видите std::move?
std::vector<AwesomeType> obj1 = ..., obj2 = ...;
const auto foo = [](auto &&) { /* ... */ };
foo(std::move(obj1)); // Вот тут прекрасно вижу, перемещение есть
foo(obj2); // А тут - не вижу. Но и никакого перемещения тут нет
Речь не о том, что внутри лямбды происходит (там в общем-то это значения не имеет). Речь о том, что перемещение всегда видно в окружении этой лямбды. В примере выше obj1
после вызова лямбды больше не содержит значений, а obj2
- содержит, и это все явно видно.
Вот эту же самую семантику хочется сохранить и для захвата: если я вижу в списке захвата мув, то значит объект перемещен в лямбду, а если не вижу, то не перемещен.
foo(std::move(obj1)); // Вот тут прекрасно вижу, перемещение есть foo(obj2); // А тут - не вижу. Но и никакого перемещения тут нет
так речь идёт об упрощении данного синтаксиса, что бы не писать длинный std:move...
foo(&&obj1);
foo(obj2);
так речь идёт об упрощении данного синтаксиса, что бы не писать длинный std:move...
Ну а я привожу аргументы против такого "упрощения". Кроме того, это не упрощение синтаксиса, а введение нового (и это большая разница).
Более того, как раз вот так foo(&&obj)
никто писать не предлагает (!!!)
Речь идёт об отдельном кейсе - списке захвата лямбды. То есть этот кейс теперь будет выделяться и причем не в лучшую сторону. В дополнение, нововведение усложнит и синтаксический анализ языка за счёт того, что вводится ещё один контекст для конструкции &&
.
К тому же, разве std::move
такой уж длинный? Не настолько, как мне кажется, чтобы вводить отдельный синтаксис. Да и не понимаю этого странного стремления укорачивать код за счет его читаемости или удобства поддержки.
Ну а я привожу аргументы против такого "упрощения". Кроме того, это не упрощение синтаксиса, а введение нового (и это большая разница).
А где граница между упрощение синтаксиса и введение нового?
Более того, как раз вот так
foo(&&obj)
никто писать не предлагает (!!!)
Это я предложил чтобы было более наглядно что имеется ввиду.
Речь идёт об отдельном кейсе - списке захвата лямбды. То есть этот кейс теперь будет выделяться и причем не в лучшую сторону.
Это субективно.
В дополнение, нововведение усложнит и синтаксический анализ языка за счёт того, что вводится ещё один контекст для конструкции
&&
.
Вы случайно не с компании которая разрабатывает анализатор? ;)
К тому же, разве
std::move
такой уж длинный?
По мне так очень длинный.
Да и не понимаю этого странного стремления укорачивать код за счет его читаемости или удобства поддержки.
1+2-3*4/6 1 плюс 2 минус 3 умножить 4 делить 6 Что удобнее?
А где граница между упрощение синтаксиса и введение нового?
Вот 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 как раз повышает читаемость и поддерживаемость такого кода
Абстрактный синтаксис не поменялся, это все еще одно и то же:
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
Если оба варианта в чём-то не очень
auto a = [variable = std::move(variable)]() {} // многословный и предлагается упростить
auto a = [&&variable]() {} // не удовлетворяет духу C++ и может запутать
То почему бы не рассмотреть третий, компромиссный?
auto a = [std::move(variable)]() {}
Если оба варианта в чём-то не очень То почему бы не рассмотреть третий, компромиссный?
что делать в случае auto a = [std::move(getMyVariable())] {}
?
Если оба варианта в чём-то не очень То почему бы не рассмотреть третий, компромиссный?
что делать в случае
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)]() { ... };
(Но у автора идеи могут быть другие мнения про множество упростимых выражений)
Во-первых, это попросту использование обобщенного синтаксиса:
const auto [var = GetAwesomeValue()] { /* ... */ }
Этот синтаксис очень многословен, к тому же со стороны с++ выглядит контринтуитивно.
К примеру эта запись уб, но нечто подобное в списке захвата считается корректным
int a = a;
В-третьих, предлагаемый подход не совсем подходит той семантике перемещения, которая в С++ существует. Перемещение в С++ явное - через вызов
std::move
, а этот кейс - вопиющее исключение. В целом это пересекается частично со сказанным выше.
А это проблема? Если ввести новвое правило, что && в лямбде это move, то всё становится довольно явно
В-четвертых, надо будет дорабатывать еще и статические анализаторы, чтобы они видели такие вот неявные мувы.
Не занимаюсь разработкой статических анализаторов, но задача выглядит тривиальной
В-пятых, получается, что можно будет одно и то же сделать по-разному. Зачем еще одно такое место?
В С++ куча способов сделать одно и то же по разному, в даном случае это сахар, потому что нынешние ляибды выглядят громоздко