Эта программа имеет четко определенное поведение и демонстрирует ошибку g ++.
Единственная сомнительная часть времени выполнения - во время инструкции (*f)();
. Поведение этой линии можно разделить по частям. Ниже приведены номера стандартных разделов: N3485; извините, если некоторые из них не соответствуют C ++ 11.
*f
- это просто встроенный унарный оператор на необработанном указателе на тип класса. Здесь нет проблем. Единственной другой оценкой является выражение функции-вызова (*f)()
, которое вызывает void std::function<void()>::operator() const
. Тогда это полное выражение является отброшенным значением.
20.8.11.2.4:
R operator()(ArgTypes... args) const
Эффекты:
blockquote>INVOKE
(obj, std::forward<ArgTypes>(args)..., R)
гдеobj
- целевой объект*this
.(Я заменил «
f
» в стандарте на «obj
», чтобы уменьшить путаницу сmain
]f
.)Здесь
obj
является копией лямбда-объекта,ArgTypes
является пустым пакетом параметров из специализацииstd::function<void()>
, аR
-void
.Псевдомакрокоманда
INVOKE
определена в 20.8.2. Поскольку типobj
не является указателем на член,INVOKE
(obj, void)
определяется какobj()
, неявно преобразованный вvoid
.5.1.2p5:
Тип замыкания для lambda-expression имеет общедоступный оператор вызова функции
blockquote>inline
...... с точно описанным объявлением. В этом случае это оказывается
void operator() const
. И его определение точно описано также:5.1.2p7:
Компонентный оператор лямбда-выражения дает функцию-тело оператора вызова функции, но для целей поиска имени, определения типа и значения
blockquote>this
и преобразования id-выражений , относящихся к нестатические члены класса в выражения доступа к членам класса, используя(*this)
, составной оператор рассматривается в контексте лямбда-выражения .5.1.2p14:
Для каждого объекта, захваченного копией, в типе замыкания объявляется неназванный нестатический элемент данных.
blockquote>5.1.2p17:
Каждое id-выражение , которое является нечетным использованием объекта, захваченного копией, преобразуется в доступ к соответствующему неназванному элементу данных из тип закрытия.
blockquote>Таким образом, оператор вызова лямбда-функции должен быть эквивалентен:
void __lambda_type::operator() const { delete __unnamed_member_f; }
(где я изобрел некоторые имена для неназванного лямбда-типа и неназванного элемента данных.)
Единственный оператор этого оператора вызова, конечно, эквивалентен
delete (*this).__unnamed_member_f;
. Итак, мы имеем:
- Разъем размытия
operator*
в унарнойoperator*
)- Выражение доступа к членству
- Вычисление значения (aka lvalue- to-rvalue) для субобъекта-члена
- Сжатие
delete
выражение Invokesstd::function<void()>::~function()
Вызываетvoid operator delete(void*)
И, наконец, в 5.3.5p4:
cast-expression в delete-expression будет оцениваться ровно один раз.
blockquote>( Здесь g ++ ошибается, делая второе вычисление значения в подобъекте элемента между вызовом деструктора и функцией освобождения.)
Этот код не может вызывать каких-либо других вычислений или побочных эффектов после выражения
delete
.Существуют некоторые допуски для поведения, определяемого реализацией в лямбда-типах и lam bda, но ничто не влияет на что-либо выше:
5.1.2p3:
Реализация может определять тип закрытия иначе, чем описано ниже, если это не изменяет наблюдаемое поведение программы, отличное от изменения:
blockquote>
- размера и / или выравнивания типа замыкания,
- , является ли тип замыкания тривиально копируемым,
- является ли тип замыкания стандартным классом макета или
- , является ли тип замыкания классом POD.
Конечно, выделение auto f = new std::function<void()>;
в порядке. Определение лямбда *f = [f]() { delete f; };
также работает, поскольку оно еще не выполнено.
Теперь интересно (*f)();
. Сначала это разметки f
, затем вызывает operator()
и, наконец, выполняет delete f
. Вызов delete f
в функции члена класса function<>::operator()
совпадает с вызовом delete this
. При определенных условиях это законно.
Так что это зависит от того, как реализовано operator()
для std::function
и lamdabs. Ваш код будет действителен, если будет гарантировано, что никакая функция-член, переменная-член или этот указатель не будет использоваться или даже не затронут operator()
после выполнения вашей инкапсулированной лямбда.
Я бы сказал, что нет необходимости для std::function
вызывать другие функции-члены или использовать переменные-члены в operator()
после выполнения вашей лямбда. Таким образом, вы, вероятно, найдете реализации, для которых ваш код является законным, но в целом, вероятно, это небезопасно.
Проблема не связана с lambdas или std :: function, а скорее с семантикой delete. Этот код имеет ту же проблему:
class A;
class B {
public:
B(A *a_) : a(a_) {}
void foo();
private:
A *const a;
};
class A {
public:
A() : b(new B(this)) {}
~A() {
delete b;
}
void foo() { b->foo(); }
private:
B *const b;
};
void B::foo() {
delete a;
}
int main() {
A *ap = new A;
ap->foo();
}
Проблема заключается в семантике удаления. Разрешено ли снова загружать операнд из памяти после вызова его деструктора, чтобы освободить его память?
Вероятно, это не произойдет в общем случае, но ПОЧЕМУ на земле вы бы хотели сделать что-то подобное в первую очередь.
Но вот мой анализ:
valgrind производит:
==7323== at 0x4008B5: _ZZ3fooILm0EEvvENKUlvE_clEv (in /home/MatsP/src/junk/a.out)
==7323== by 0x400B4A: _ZNSt17_Function_handlerIFvvEZ3fooILm0EEvvEUlvE_E9_M_invokeERKSt9_Any_data (in /home/MatsP/src/junk/a.out)
==7323== by 0x4009DB: std::function<void ()()>::operator()() const (in /home/MatsP/src/junk/a.out)
==7323== by 0x40090A: void foo<0ul>() (in /home/MatsP/src/junk/a.out)
==7323== by 0x4007E8: main (in /home/MatsP/src/junk/a.out)
Это указывает на код здесь (который действительно является лямбда-функцией в вашем исходном коде) :
000000000040088a <_ZZ3fooILm0EEvvENKUlvE_clEv>:
40088a: 55 push %rbp
40088b: 48 89 e5 mov %rsp,%rbp
40088e: 48 83 ec 10 sub $0x10,%rsp
400892: 48 89 7d f8 mov %rdi,-0x8(%rbp)
400896: 48 8b 45 f8 mov -0x8(%rbp),%rax
40089a: 48 8b 00 mov (%rax),%rax
40089d: 48 85 c0 test %rax,%rax ;; Null check - don't delete if null.
4008a0: 74 1e je 4008c0 <_ZZ3fooILm0EEvvENKUlvE_clEv+0x36>
4008a2: 48 8b 45 f8 mov -0x8(%rbp),%rax
4008a6: 48 8b 00 mov (%rax),%rax
4008a9: 48 89 c7 mov %rax,%rdi
;; Call function destructor
4008ac: e8 bf ff ff ff callq 400870 <_ZNSt8functionIFvvEED1Ev>
4008b1: 48 8b 45 f8 mov -0x8(%rbp),%rax
4008b5: 48 8b 00 mov (%rax),%rax ;; invalid access
4008b8: 48 89 c7 mov %rax,%rdi
;; Call delete.
4008bb: e8 b0 fd ff ff callq 400670 <_ZdlPv@plt> ;; delete
4008c0: c9 leaveq
4008c1: c3 retq
Интересно, что «работает» с использованием clang ++ (версия 3.5, построенная из git sha1 d73449481daee33615d907608a3a08548ce2ba65, с 31 марта):
0000000000401050 <_ZZ3fooILm0EEvvENKUlvE_clEv>:
401050: 55 push %rbp
401051: 48 89 e5 mov %rsp,%rbp
401054: 48 83 ec 10 sub $0x10,%rsp
401058: 48 89 7d f8 mov %rdi,-0x8(%rbp)
40105c: 48 8b 7d f8 mov -0x8(%rbp),%rdi
401060: 48 8b 3f mov (%rdi),%rdi
401063: 48 81 ff 00 00 00 00 cmp $0x0,%rdi ;; Null check.
40106a: 48 89 7d f0 mov %rdi,-0x10(%rbp)
40106e: 0f 84 12 00 00 00 je 401086 <_ZZ3fooILm0EEvvENKUlvE_clEv+0x36>
401074: 48 8b 7d f0 mov -0x10(%rbp),%rdi
401078: e8 d3 fa ff ff callq 400b50 <_ZNSt8functionIFvvEED2Ev>
;; Function destructor
40107d: 48 8b 7d f0 mov -0x10(%rbp),%rdi
401081: e8 7a f6 ff ff callq 400700 <_ZdlPv@plt> ;; delete.
401086: 48 83 c4 10 add $0x10,%rsp
40108a: 5d pop %rbp
40108b: c3 retq
Редактировать: действительно имеет смысл - я не понимаю, почему есть доступ к памяти для первого элемента внутри класса функции в коде gcc, а не в clang's - они должны делать то же самое ...
f
дважды. Я не уверен, что это законно в модели памяти C ++ 11.
– Puppy
10 April 2014 в 23:44
Это, конечно, не совсем корректное поведение в целом.
Между окончанием выполнения функционального объекта и окончанием вызова operator()
член operator()
выполняется на удаленный объект. Если реализация читает или записывает через this
, что вполне разрешено делать, тогда вы получите чтение или запись удаленного объекта.
Более конкретно, объект был просто удален этим очень ничто, поэтому маловероятно, чтобы какой-либо поток действительно обходил его использование между удалением и чтением / записью, или он был неотображаем, поэтому вряд ли это действительно вызовет проблемы в простой программе. Кроме того, существует небольшая очевидная причина для реализации, чтобы читать или записывать в this
после ее возвращения.
Однако Valgrind вполне корректен, что любое такое чтение или запись будет очень недействительный и в некоторых случаях может привести к случайным сбоям или повреждению памяти. Легко предположить, что между удалением this
и гипотетическим чтением / записью этот поток был предварительно упущен, а другой поток назначен и использовал эту память. В качестве альтернативы, распределитель памяти решил, что у него достаточно кэшированной памяти такого размера, и сразу же освободил этот сегмент для ОС. Это отличный кандидат на Гейзенбуг, поскольку условия его возникновения будут относительно редкими и очевидны только в реальных сложных системах исполнения, а не в тривиальных тестовых программах.
Вы могли бы избежать этого , если вы можете доказать, что чтения или записи нет после завершения объекта функции. Это в основном означает гарантию реализации std::function<Sig>::operator()
.
Редактировать:
Ответ Матса Петерсона вызывает интересный вопрос. GCC, похоже, реализовал лямбда, выполнив что-то вроде этого:
struct lambda { std::function<void()>* f; };
void lambda_operator(lambda* l) {
l->f->~std::function<void()>();
::operator delete(l->f);
}
Как вы можете видеть, вызов operator delete
выполняет загрузку с l
после того, как он только что был удален, что точно описанный выше сценарий. Я на самом деле не уверен, что правила модели памяти C ++ 11 говорят об этом, я бы подумал, что это незаконно, но не обязательно. Он не может быть определен в любом случае. Если это не является незаконным, вы определенно ввернуты.
Тем не менее Clang, похоже, сгенерирует этот оператор:
void lambda_operator(lambda* l) {
auto f = l->f;
f->~std::function<void()>();
::operator delete(f);
}
Здесь, когда l
удален, это не имеет значения потому что f
был скопирован в локальное хранилище.
В определенной степени это окончательно отвечает на ваш вопрос. GCC абсолютно загружает из памяти лямбда после ее удаления. Является ли это стандартным или нет, я не уверен. Вы определенно можете обойти это, используя определенную пользователем функцию. Тем не менее, проблема с реализацией std :: function, выполняющей загрузку или сохранение в хранилище до this
.
this
во время вызова функции-члена, это ваша проблема, а не стандартная библиотека, например, любая другая, если она хорошо сформирована.
– alphashooter
11 April 2014 в 01:57
this
после возвращения функционального объекта operator()
. И это реализация, поэтому они могут реализовать operator()
, как им нравится. Никакая функция не может считаться законной для работы с удаленным объектом, если явно не указано иное.
– Puppy
11 April 2014 в 12:33
std::function
после вызова в operator()
. Фактический доступ в течение всей жизни.
– Casey
12 April 2014 в 02:31
См. http://cplusplus.github.io/LWG/lwg-active.html#2224 .
Доступ к типу библиотеки после запуска деструктора - это неопределенное поведение , Lambdas не являются библиотечными типами, поэтому у них нет такого ограничения. Когда введен деструктор типа библиотеки, инварианты этого типа библиотеки больше не сохраняются. Язык не применяет такое ограничение, поскольку инварианты по большей части являются концепцией библиотеки, а не понятием языка.
delete this
. он удаляет указатель, который оказывается равнымthis
. Это не гарантия, предусмотреннаяconst
. Точно так же вы можете иметь указатель на объект и указатель на тот же объект, что и const, и вызывать удаление по нормальному указателю. – user 11 April 2014 в 05:00std::function::operator()
читать или записывать черезthis
после возвращения выражения INVOKE. – Puppy 11 April 2014 в 12:35INVOKE
должно быть единственным эффектом вызоваoperator()
. – user 11 April 2014 в 15:35std::function::operator()
запустить ядерный арсенал США в Лихтенштейне после того, как выражение INVOKE вернулось. Это не нужно делать, потому что описания поведения в стандарте считаются общими. – Casey 12 April 2014 в 02:24