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(); }
, чем создавать структуру, которая делает то же самое.
Возможно, вы могли бы пояснить, почему лямбда, которая никогда не возвращает нулевое значение, должна быть конструируемой по умолчанию, создавая лямбду, которая делает возвращает нулевое значение? Это не кажется желательным или интуитивным.
Что бы вы сделали, если бы захваченный объект не имел значения/конструктора по умолчанию? Что вы делаете для ссылок?
Для какого алгоритма требуется конструктивный функтор по умолчанию? Насколько я знаю, они требуют, чтобы они копировали конструктивно.
Я уверен, что есть и другие. Но такие вещи, как std::upper_bound
, требуют, чтобы тип Iterator
был DefaultConstructible, потому что он, вероятно, хочет создать временный It middle
. Если бы я создал TransformIterator<T, F>
, который применяет функцию преобразования, то использование лямбды не сработало бы. Но использование структуры будет.
Для этого вам все равно нужно написать структуру, потому что у типа Iterator
есть требования, которым лямбда не удовлетворит.
Лямбда — это сокращение для написания функтора. TransformIterator
при выполнении преобразования также должен быть итератором, а это не то, для чего создается лямбда-выражение.
Я имел в виду, что лямбда будет типом F
, а класс TransformIterator<It, F> {It it; F f;}
будет удовлетворять всем остальным.
Для чего-то подобного вы можете избавиться от 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; };
Я знаю это, но std::function
как некоторые накладные расходы. В любом случае, меня не волнует вариант использования, мне просто интересно, почему стандарт просто не добавляет конструктор `= default` к типу закрытия лямбда.
Показательный пример выполнения std::function<int*(void)>([p{std::make_unique<int>(0)}](){ return p.get(); } )
в основном делает то же самое, оборачивая лямбду во что-то с помощью конструктора = default
, где вызов сконструированного объекта по умолчанию не рекомендуется, но все еще имеет допустимый синтаксис.
int
0
или неинициализированным? Какой будет созданная по умолчанию ссылка Справка?
@DrewDormann Не мог ли тип замыкания определить конструктор = default
, и компилятор обработал бы это обычными методами? то есть: если вы захватите ссылку, она не сможет скомпилироваться. Но если захваченный объект DefaultConstructible, это сработает.
Я предполагаю, что что-то вроде этого godbolt.org/z/Tc75aY6f4 может быть выполнимо?
@Deev: Обратите внимание, что std::transform_view
требует regular_invocable
, который имеет встроенное требование либо быть полностью апатридом, либо изменение состояния не влияет на равенство вывода. Поэтому, если «захваченное» значение построено по умолчанию, это может представлять другой результат. Тем самым нарушая regular_invocable
.
Есть две причины этого не делать: концептуальная и безопасность.
Несмотря на пожелания некоторых программистов на C++, лямбда-выражения не должны быть коротким синтаксисом для структуры с перегруженным operator()
. Это то, из чего состоят лямбда-выражения С++, но не лямбда-выражения находятся.
Концептуально лямбда C++ должна быть приближением C++ к лямбда-функция. Функциональность захвата не предназначена для записи членов структуры; предполагается, что он имитирует надлежащие возможности лексической области видимости лямбда-выражений. Вот почему они существуют.
Создание такой лямбды (изначально не путем копирования/перемещения существующей) за пределами лексической области, в которой она была определена, концептуально бессмысленно. Нет смысла писать что-то, привязанное к лексической области видимости, а затем создавать ее за пределами той области, для которой она была создана.
Вот почему вы не можете получить доступ к этим членам за пределами лямбды. Потому что, даже если они могут быть публичными членами, они существуют для реализации надлежащей лексической области видимости. Это детали реализации.
Построение «лямбды», которая «захватывает переменные» без фактического захвата чего-либо, имеет смысл только с точки зрения метапрограммирования. То есть это имеет смысл, только если сосредоточиться на том, из чего сделаны лямбда-выражения, а не на том, что они из себя представляют. Лямбда реализована как структура C++ с захватами в качестве членов, а выражения захвата технически даже не должны называть локальные переменные, поэтому эти члены теоретически могут быть инициализированы значением.
Если вас не убедил концептуальный аргумент, давайте поговорим о безопасности. Что вы хотите сделать, так это объявить, что любая лямбда должна быть конструируемой по умолчанию, если все ее захваты не являются эталонными захватами и относятся к конструируемым типам по умолчанию. Это приглашает стихийное бедствие. Почему?
Потому что автор многих таких лямбд не просил об этом. Если лямбда захватывает unique_ptr<T>
, перемещаясь из переменной, указывающей на объект, для кода внутри этой лямбды на 100% допустимо (в соответствии с текущими правилами) предположить, что захваченное значение указывает на объект. Конструкция по умолчанию, хотя и синтаксически допустима, в этом случае семантически бессмысленна.
С правильно названным типом пользователь может легко контролировать, является ли он конструируемым по умолчанию или нет. И поэтому, если не имеет смысла конструировать тот или иной тип по умолчанию, они могут это запретить. С лямбда-выражениями такого синтаксиса нет; вы должны навязать ответ всем. И ответ самый безопасный для захвата лямбда-выражений, который гарантированно никогда не сломает код, — «нет».
Напротив, конструкция лямбда-выражений без захвата по умолчанию никогда не может быть неправильной. Такие функции «чисты» (относительно содержимого функтора, так как функтор не имеет содержимого). Это также согласуется с приведенным выше концептуальным аргументом: лямбда без захвата не имеет надлежащей лексической области видимости, и поэтому ее порождение в любом месте, даже за пределами ее исходной области, допустимо.
Если вам нужно поведение именованной структуры... просто создайте именованную структуру. Вам даже не нужно default
конструктор по умолчанию; вы получите его по умолчанию (если вы не объявляете никаких других конструкторов).
Ты держал меня до последнего слова, где я думал, что захваченный
p
будетmake_unique<int>(0)
. Потому что если позволитьp
бытьnullptr
, это нарушит инвариант. Захваты лямбды всегда устанавливаютp
непустымunique_ptr
. Тело лямбды может предполагать, чтоp
никогда не бывает нулевым, и если лямбда, сконструированная по умолчанию, имеет нулевое значениеp
, будет ошибкой, которую будет почти невозможно отследить: вы просматриваете каждое использование лямбды, и она всегда инициализируется.p
, но каким-то образом через 10 минут вы оказываетесь перед экземпляром лямбды с нулевым значениемp
!