Можно ли удалить объект std :: function из его целевой функции во время вызова? [Дубликат]

10
задан 10 April 2014 в 22:36
поделиться

6 ответов

Эта программа имеет четко определенное поведение и демонстрирует ошибку g ++.

Единственная сомнительная часть времени выполнения - во время инструкции (*f)();. Поведение этой линии можно разделить по частям. Ниже приведены номера стандартных разделов: N3485; извините, если некоторые из них не соответствуют C ++ 11.

*f - это просто встроенный унарный оператор на необработанном указателе на тип класса. Здесь нет проблем. Единственной другой оценкой является выражение функции-вызова (*f)(), которое вызывает void std::function<void()>::operator() const. Тогда это полное выражение является отброшенным значением.

20.8.11.2.4:

R operator()(ArgTypes... args) const

Эффекты: 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 имеет общедоступный оператор вызова функции inline ...

... с точно описанным объявлением. В этом случае это оказывается void operator() const. И его определение точно описано также:

5.1.2p7:

Компонентный оператор лямбда-выражения дает функцию-тело оператора вызова функции, но для целей поиска имени, определения типа и значения this и преобразования id-выражений , относящихся к нестатические члены класса в выражения доступа к членам класса, используя (*this), составной оператор рассматривается в контексте лямбда-выражения .

5.1.2p14:

Для каждого объекта, захваченного копией, в типе замыкания объявляется неназванный нестатический элемент данных.

5.1.2p17:

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

Таким образом, оператор вызова лямбда-функции должен быть эквивалентен:

void __lambda_type::operator() const {
    delete __unnamed_member_f;
}

(где я изобрел некоторые имена для неназванного лямбда-типа и неназванного элемента данных.)

Единственный оператор этого оператора вызова, конечно, эквивалентен delete (*this).__unnamed_member_f;. Итак, мы имеем:

  • Разъем размытия operator* в унарной operator*)
  • Выражение доступа к членству
  • Вычисление значения (aka lvalue- to-rvalue) для субобъекта-члена
  • Сжатие delete выражение Invokes std::function<void()>::~function() Вызывает void operator delete(void*)

И, наконец, в 5.3.5p4:

cast-expression в delete-expression будет оцениваться ровно один раз.

( Здесь g ++ ошибается, делая второе вычисление значения в подобъекте элемента между вызовом деструктора и функцией освобождения.)

Этот код не может вызывать каких-либо других вычислений или побочных эффектов после выражения delete .

Существуют некоторые допуски для поведения, определяемого реализацией в лямбда-типах и lam bda, но ничто не влияет на что-либо выше:

5.1.2p3:

Реализация может определять тип закрытия иначе, чем описано ниже, если это не изменяет наблюдаемое поведение программы, отличное от изменения:

  • размера и / или выравнивания типа замыкания,
  • , является ли тип замыкания тривиально копируемым,
  • является ли тип замыкания стандартным классом макета или
  • , является ли тип замыкания классом POD.
8
ответ дан aschepler 19 August 2018 в 03:27
поделиться
  • 1
    Чтобы ответить на это, вам нужно прорыть код g ++. – aschepler 11 April 2014 в 02:45
  • 2
    Это не буквально вызывает delete this. он удаляет указатель, который оказывается равным this. Это не гарантия, предусмотренная const. Точно так же вы можете иметь указатель на объект и указатель на тот же объект, что и const, и вызывать удаление по нормальному указателю. – user 11 April 2014 в 05:00
  • 3
    Это не определяет, может ли оператор std::function::operator() читать или записывать через this после возвращения выражения INVOKE. – Puppy 11 April 2014 в 12:35
  • 4
    @DeadMG Я думаю, что стандарт явно указывает здесь, что выражение INVOKE должно быть единственным эффектом вызова operator(). – user 11 April 2014 в 15:35
  • 5
    @DeadMG Он не говорит, может ли std::function::operator() запустить ядерный арсенал США в Лихтенштейне после того, как выражение INVOKE вернулось. Это не нужно делать, потому что описания поведения в стандарте считаются общими. – Casey 12 April 2014 в 02:24

Конечно, выделение 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() после выполнения вашей лямбда. Таким образом, вы, вероятно, найдете реализации, для которых ваш код является законным, но в целом, вероятно, это небезопасно.

0
ответ дан Danvil 19 August 2018 в 03:27
поделиться

Проблема не связана с 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();
}

Проблема заключается в семантике удаления. Разрешено ли снова загружать операнд из памяти после вызова его деструктора, чтобы освободить его память?

2
ответ дан kec 19 August 2018 в 03:27
поделиться
  • 1
    Хорошая находка. В этом случае все сводится к 5.3.5p4 «Литье-выражение в delete-expression должно оцениваться ровно один раз. & Quot; Я читаю & quot; оцениваю & quot; как включающий все виды действий 3.8 запрещает указатель на / glvalue, ссылающийся на уничтоженный объект. – aschepler 11 April 2014 в 02:48
  • 2
    @aschepler: Я думаю, что ваше чтение правильно. Фактически это было в каком-то коде, который был представлен клиентом18 для класса, который я преподаю. Мы пытались отследить, если это был его код, который был несовместим, или g ++, который не соответствовал требованиям. (Мы знали, что clang ++ в порядке). – kec 11 April 2014 в 03:00

Вероятно, это не произойдет в общем случае, но ПОЧЕМУ на земле вы бы хотели сделать что-то подобное в первую очередь.

Но вот мой анализ:

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 - они должны делать то же самое ...

1
ответ дан Mats Petersson 19 August 2018 в 03:27
поделиться
  • 1
    Интересно - компилятор загрузил f дважды. Я не уверен, что это законно в модели памяти C ++ 11. – Puppy 10 April 2014 в 23:44
  • 2
    Глядя на функциональный заголовочный файл (скорее, беспорядочный - я уверен, по уважительной причине), похоже, есть какой-то _M_access (но может быть, только если функция имеет фактические аргументы?). – Mats Petersson 11 April 2014 в 00:19

Это, конечно, не совсем корректное поведение в целом.

Между окончанием выполнения функционального объекта и окончанием вызова 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.

3
ответ дан Puppy 19 August 2018 в 03:27
поделиться
  • 1
    «Если реализация считывает или записывает через это, что вполне разрешено делать». Почему это разрешено? В конце концов, если это так, то поведение программы становится неопределенным! – Johannes Schaub - litb 11 April 2014 в 00:45
  • 2
    @ JohannesSchaub-litb Потому что это реализация стандартной библиотеки. Он хорошо сформирован, поэтому он должен иметь предсказуемое поведение. И если вы используете , то вы must обеспечиваете его поведение, определенное в вашей программе. Вы используйте его. Если для вас плохо, что этот объект разрешил читать / записывать через this во время вызова функции-члена, это ваша проблема, а не стандартная библиотека, например, любая другая, если она хорошо сформирована. – alphashooter 11 April 2014 в 01:57
  • 3
    @Johannes: В стандарте, запрещающем реализацию, не существует формулировки, заключающейся в выдаче чтений и записи через this после возвращения функционального объекта operator(). И это реализация, поэтому они могут реализовать operator(), как им нравится. Никакая функция не может считаться законной для работы с удаленным объектом, если явно не указано иное. – Puppy 11 April 2014 в 12:33
  • 4
    @ JohannesSchaub-litb Я не считаю, что резолюция 2224 будет применяться к этому делу. Нормативный оператор «Если к объекту стандартного типа библиотеки обращается [основное внимание], а начало срока жизни объекта (3.8 [basic.life]) не происходит до доступа или доступ не выполняется до конца срока службы объекта, поведение не определено, если не указано иное. & quot; поскольку - как объясняется в ответе ачеллера - нет доступа к std::function после вызова в operator(). Фактический доступ в течение всей жизни. – Casey 12 April 2014 в 02:31
  • 5
    @Casey в списке std-обсуждения, я и другие указали это также (что коренится на небрежном использовании «доступа»). Это по-прежнему отчет о открытых проблемах, поэтому предлагаемое решение не обязательно отражает мнение комитета с большой вероятностью (как это верно при разрешении проблем, которые вводятся в спецификацию). Разница между примечаниями, что являются в спецификации, которые do выражают намерение комитета с высокой вероятностью, а не нормативно . И в нашем случае примечание в спецификации делает код в этом вопросе неопределенным. – Johannes Schaub - litb 12 April 2014 в 15:42

См. http://cplusplus.github.io/LWG/lwg-active.html#2224 .

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

2
ответ дан Ville Voutilainen 19 August 2018 в 03:27
поделиться
Другие вопросы по тегам:

Похожие вопросы: