Удаление объекта std :: function внутри себя

Является ли это четко определенным поведением?

#include <functional>

void foo() {
    auto f = new std::function<void()>;
    *f = [f]() { delete f; };
    (*f)();
    f = nullptr;
}

int main() {
    foo();
}

Используя последний g++, если я делаю это в шаблоне, он вызывает недопустимые чтения во время работы под valgrind, иначе он отлично работает. Зачем? Это ошибка в g++?

#include <functional>

template<std::size_t>
void foo() {
    auto f = new std::function<void()>;
    *f = [f]() { delete f; };
    (*f)();
    f = nullptr;
}

int main() {
    foo<0>();
}

Ответ 1

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

Единственная сомнительная часть времени выполнения - во время утверждения (*f)(); , Поведение этой линии можно разделить по частям. Ниже приведены номера стандартных разделов: N3485; извините, если некоторые не соответствуют С++ 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:

Тип замыкания для лямбда-выражения имеет открытый 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* разыменовывается (по значению this)
  • Выражение доступа к члену
  • Вычисление значения (aka lvalue-to-rvalue conversion) для субобъекта участника
  • Скалярное выражение delete
    • Вызывает std::function<void()>::~function()
    • Вызывает void operator delete(void*)

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

Выражение-выражение в выражении-удалении должно оцениваться ровно один раз.

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

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

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

5.1.2p3:

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

  • размер и/или выравнивание типа закрытия,

  • является ли тип замыкания тривиально скопируемым,

  • является ли тип закрытия стандартным классом макета или

  • является ли тип замыкания классом POD.

Ответ 2

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

Между завершением выполнения объекта функции и окончанием вызова operator() 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 после ее удаления, что является именно тем сценарием, который я описал выше. Я на самом деле не уверен, что правила модели памяти С++ 11 говорят об этом, я бы подумал, что это незаконно, но не обязательно. Он не может быть определен в любом случае. Если это не незаконно, вы определенно ввернуты.

Тем не менее, Clang, похоже, генерирует этот оператор:

void lambda_operator(lambda* l) {
    auto f = l->f;
    f->~std::function<void()>();
    ::operator delete(f);
}

Здесь, когда l удаляется, это не имеет значения, поскольку f был скопирован в локальное хранилище.

В определенной степени это окончательно отвечает на ваш question- GCC абсолютно загружается из памяти лямбда после его удаления. Является ли этот Стандарт законным или нет, я не уверен. Вы определенно можете обойти это, используя определенную пользователем функцию. Вы все еще есть проблема реализации станд :: функции, выдавшего нагрузки или магазины в this, хотя.

Ответ 3

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

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

Ответ 4

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

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

Ответ 5

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

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

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 <[email protected]>   ;; 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 <[email protected]>    ;; delete. 
  401086:   48 83 c4 10             add    $0x10,%rsp
  40108a:   5d                      pop    %rbp
  40108b:   c3                      retq   

Редактировать: на самом деле это не имеет никакого смысла - я не понимаю, почему есть доступ к памяти для первого элемента внутри класса функции в коде gcc, а не в clang - они должны делать то же самое...

Ответ 6

Выделение auto f = new std::function<void()>; конечно, хорошо. Определение лямбда *f = [f]() { delete f; }; *f = [f]() { delete f; }; также работает, поскольку он еще не выполнен.

Теперь интересно (*f)(); , Сначала это разрывы f, затем вызывает operator() и, наконец, выполняется delete f. Вызов delete f в функции- function<>::operator() члена класса function<>::operator() совпадает с вызовом delete this. При определенных обстоятельствах это законно.

Таким образом, это зависит от того, как реализуется operator() для std::function и lamdabs. Ваш код будет действителен, если будет гарантировано, что никакая функция-член, переменная-член или этот указатель не будут использоваться или даже затронуты operator() после выполнения вашей инкапсулированной лямбда.

Я бы сказал, что std::function требует вызова других функций-членов или использования переменных-членов в operator() после выполнения вашей лямбда. Таким образом, вы, вероятно, найдете реализации, для которых ваш код является законным, но в целом, вероятно, это небезопасно.