Почему лямбда-выражения с копированием не являются DefaultConstructible по умолчанию в С++ 20

C++20 представляет DefaultConstructible лямбда-выражения. Однако cppreference.com заявляет, что это только для лямбда-выражений без сохранения состояния:

If no captures are specified, the closure type has a defaulted default constructor. Otherwise, it has no default constructor (this includes the case when there is a capture-default, even if it does not actually capture anything).

Почему это не распространяется на лямбда-выражения, которые захватывают то, что DefaultConstructible? Например, почему [p{std::make_unique<int>(0)}](){ return p.get(); } не может быть DefaultConstructible, где захваченное p было бы nullptr?

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

struct S{
  S() = default;
  int* operator()() const { return p.get(); }
  std::unique_ptr<int> p;
};

Итак, мы можем передать S{std::make_unique<int>(0)}, что делает то же самое.

Кажется, что было бы намного лучше иметь возможность написать [p{std::make_unique<int>(0)}](){ return p.get(); }, чем создавать структуру, которая делает то же самое.

Ты держал меня до последнего слова, где я думал, что захваченный p будет make_unique<int>(0). Потому что если позволить p быть nullptr, это нарушит инвариант. Захваты лямбды всегда устанавливают p непустым unique_ptr. Тело лямбды может предполагать, что p никогда не бывает нулевым, и если лямбда, сконструированная по умолчанию, имеет нулевое значение p, будет ошибкой, которую будет почти невозможно отследить: вы просматриваете каждое использование лямбды, и она всегда инициализируется. p, но каким-то образом через 10 минут вы оказываетесь перед экземпляром лямбды с нулевым значением p!

Raymond Chen 22.04.2022 23:12

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

Drew Dormann 22.04.2022 23:13

Что бы вы сделали, если бы захваченный объект не имел значения/конструктора по умолчанию? Что вы делаете для ссылок?

NathanOliver 22.04.2022 23:14

Для какого алгоритма требуется конструктивный функтор по умолчанию? Насколько я знаю, они требуют, чтобы они копировали конструктивно.

NathanOliver 22.04.2022 23:22

Я уверен, что есть и другие. Но такие вещи, как std::upper_bound, требуют, чтобы тип Iterator был DefaultConstructible, потому что он, вероятно, хочет создать временный It middle. Если бы я создал TransformIterator<T, F>, который применяет функцию преобразования, то использование лямбды не сработало бы. Но использование структуры будет.

Deev 22.04.2022 23:26

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

NathanOliver 22.04.2022 23:30

Лямбда — это сокращение для написания функтора. TransformIterator при выполнении преобразования также должен быть итератором, а это не то, для чего создается лямбда-выражение.

NathanOliver 22.04.2022 23:31

Я имел в виду, что лямбда будет типом F, а класс TransformIterator<It, F> {It it; F f;} будет удовлетворять всем остальным.

Deev 22.04.2022 23:32

Для чего-то подобного вы можете избавиться от F и использовать std::function вместо template <typename It> struct TransformIterator { using value_type = typename std::iteator_traits<It>::value_type; It it; std::function<value_type(value_type)> f; };

NathanOliver 22.04.2022 23:38

Я знаю это, но std::function как некоторые накладные расходы. В любом случае, меня не волнует вариант использования, мне просто интересно, почему стандарт просто не добавляет конструктор `= default` к типу закрытия лямбда.

Deev 22.04.2022 23:44

Показательный пример выполнения std::function<int*(void)>([p{std::make_unique<int>(0)}](){ return p.get(); } ) в основном делает то же самое, оборачивая лямбду во что-то с помощью конструктора = default, где вызов сконструированного объекта по умолчанию не рекомендуется, но все еще имеет допустимый синтаксис.

Deev 23.04.2022 00:12
Первый черновик документа, предлагающего «конструируемые лямбда-выражения по умолчанию», включает это ограничение. Можно предположить, что это связано с тем, что лямбда без захвата всегда может быть построена по умолчанию, в то время как лямбда с захватами вносит много сложностей. Будет ли захваченный int0 или неинициализированным? Какой будет созданная по умолчанию ссылка Справка?
Drew Dormann 23.04.2022 00:15

@DrewDormann Не мог ли тип замыкания определить конструктор = default, и компилятор обработал бы это обычными методами? то есть: если вы захватите ссылку, она не сможет скомпилироваться. Но если захваченный объект DefaultConstructible, это сработает.

Deev 23.04.2022 00:17

Я предполагаю, что что-то вроде этого godbolt.org/z/Tc75aY6f4 может быть выполнимо?

Deev 23.04.2022 00:24

@Deev: Обратите внимание, что std::transform_view требует regular_invocable, который имеет встроенное требование либо быть полностью апатридом, либо изменение состояния не влияет на равенство вывода. Поэтому, если «захваченное» значение построено по умолчанию, это может представлять другой результат. Тем самым нарушая regular_invocable.

Nicol Bolas 23.04.2022 05:02
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
15
101
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Есть две причины этого не делать: концептуальная и безопасность.

Несмотря на пожелания некоторых программистов на C++, лямбда-выражения не должны быть коротким синтаксисом для структуры с перегруженным operator(). Это то, из чего состоят лямбда-выражения С++, но не лямбда-выражения находятся.

Концептуально лямбда C++ должна быть приближением C++ к лямбда-функция. Функциональность захвата не предназначена для записи членов структуры; предполагается, что он имитирует надлежащие возможности лексической области видимости лямбда-выражений. Вот почему они существуют.

Создание такой лямбды (изначально не путем копирования/перемещения существующей) за пределами лексической области, в которой она была определена, концептуально бессмысленно. Нет смысла писать что-то, привязанное к лексической области видимости, а затем создавать ее за пределами той области, для которой она была создана.

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

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

Если вас не убедил концептуальный аргумент, давайте поговорим о безопасности. Что вы хотите сделать, так это объявить, что любая лямбда должна быть конструируемой по умолчанию, если все ее захваты не являются эталонными захватами и относятся к конструируемым типам по умолчанию. Это приглашает стихийное бедствие. Почему?

Потому что автор многих таких лямбд не просил об этом. Если лямбда захватывает unique_ptr<T>, перемещаясь из переменной, указывающей на объект, для кода внутри этой лямбды на 100% допустимо (в соответствии с текущими правилами) предположить, что захваченное значение указывает на объект. Конструкция по умолчанию, хотя и синтаксически допустима, в этом случае семантически бессмысленна.

С правильно названным типом пользователь может легко контролировать, является ли он конструируемым по умолчанию или нет. И поэтому, если не имеет смысла конструировать тот или иной тип по умолчанию, они могут это запретить. С лямбда-выражениями такого синтаксиса нет; вы должны навязать ответ всем. И ответ самый безопасный для захвата лямбда-выражений, который гарантированно никогда не сломает код, — «нет».

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

Если вам нужно поведение именованной структуры... просто создайте именованную структуру. Вам даже не нужно default конструктор по умолчанию; вы получите его по умолчанию (если вы не объявляете никаких других конструкторов).

Другие вопросы по теме