Блочный синтаксис был введен Apple в 2008 году и с тех пор постепенно внедряется во фреймворки операционных систем. Он предшествовал лямбдам в C ++ (который появился в C ++ 11) и совместим с C, Objective C и Objective C ++.
Об использовании блоков в Objective C написано много документации, например, печально известный сайт черт возьми, синтаксис блоков и опасности сохранения циклов в блоках. Шаблон сильное-я / слабое-я хорошо известен программистам на Objective C, но, похоже, очень мало написано о безопасном использовании (Objective) C ++ с блоками, что, к сожалению, очень легко испортить. вверх.
Возьмем «простой» пример с Objective C:
__weak __typeof__(self) weakSelf = self; dispatch_group_async(_operationsGroup, _operationsQueue, ^ { __typeof__(self) strongSelf = weakSelf; [strongSelf doSomething]; });
Как бы мы закодировали это с помощью классов C ++ вместо классов Objective C. Что-то вроде:
__weak __typeof__(this) weakThis = this; dispatch_group_async(_operationsGroup, _operationsQueue, ^ { __typeof__(this) strongThis = this; strongThis->DoSomething(); });
Обратите внимание, что приведенный выше код компилируется и «запускается» (при условии, что вы определили необходимые биты и части) и выдает предупреждение только в Xcode 10.1:
‘__weak’ only applies to Objective-C object or block pointer types; type here is ‘typeof (this)’ (aka ‘MyClass *’)
Это прискорбно, потому что игнорирование предупреждения очень опасно (могу ли я предложить -Werror=ignored-attributes
сделать это ошибкой). Квалификатор __weak
здесь является полным запретом на использование указателя this
C ++.
Официально это неопределенное поведение - выполнение операции со слабой семантикой присваивания с указателем на объект Objective-C, класс которого не поддерживает
__weak
ссылки. Я не смог найти ничего в документации ARC по__weak
по указателю на объект, не относящийся к Objective-C, так что я полагаю, что это также делает его неопределенным.
Таким образом, семантически (игнорируя неопределенное поведение и сосредотачиваясь на наблюдаемом поведении) приведенный выше код эквивалентен следующему:
dispatch_group_async(_operationsGroup, _operationsQueue, ^ { this->DoSomething(); });
который, к сожалению, компилируется чисто без каких-либо предупреждений / ошибок.
Хорошая новость об этом коде заключается в том, что мы не можем получить цикл сохранения в C ++, потому что здесь нет подсчета ссылок. Плохая новость заключается в том, что если наш объект C ++ будет удален до того, как наш блок будет завершен, указатель this
будет висящим плохим указателем.
Следует отметить, что есть часть Спецификации блока, посвященная объектам C ++, в которой говорится: Для поддержки доступа к переменным-членам и функциям компилятор будет синтезировать указатель const
на блочную версию указателя this
. Это означает, что приведенный выше блок можно еще больше упростить:
dispatch_group_async(_operationsGroup, _operationsQueue, ^ { DoSomething(); });
но он по-прежнему содержит (возможно, даже более скрытую из-за неявного this) проблему с висячим указателем. Также обратите внимание, что это const
указатель на this
в отличие от const
указателя на const
this
, поэтому объект, на который указывает this
, является изменяемым.
Итак, нам нужно нечто семантически эквивалентное шаблону «сильное я / слабое я», которое мы можем использовать в C ++.
В C ++ действительно есть шаблон сильный-я / слабый-я с использованием std :: shared_ptr / std :: weak_ptr, который, кажется, отвечает всем требованиям. Мы можем наивно подумать, что хотим что-то вроде этого:
std::weak_ptr<MyClass> weak_this(this); dispatch_group_async(_operationsGroup, _operationsQueue, ^ { auto strong_this = weak_this.lock(); strong_this->DoSomething(); });
К сожалению, не существует weak_ptr
конструктора, который принимает голый указатель, поэтому он вообще не компилируется. Единственный способ получить weak_ptr
- это shared_ptr
. У вас может возникнуть соблазн сделать:
std::shared_ptr<MyClass> shared_this(this); std::weak_ptr<MyClass> weak_this(shared_this); dispatch_group_async(_operationsGroup, _operationsQueue, ^ { auto strong_this = weak_this.lock(); strong_this->DoSomething(); });
который отлично скомпилируется. К сожалению, вы обнаружите, что ваш this
указатель освобождается, когда shared_this
выходит за пределы области видимости, что, вероятно, означает, что он будет освобожден дважды (один раз, когда shared_ptr
выходит из области видимости, и один раз, когда выделенный this
решает избавиться от него), и очень маловероятно, что weak_this
когда-либо даст вам strong_this
, потому что shared_this
, скорее всего, выйдет за пределы области действия до того, как асинхронный блок получит возможность запуститься.
По сути, это проблема с std::shared_ptr
в том, что получение std::shared_ptr
для this
невозможно для this
, которым уже управляет std::shared_ptr
, без ссылки на это управление std::shared_ptr
. К счастью, в стандарте C ++ есть решение для нас, и это решение - std :: enable_shared_from_this.
Это означает, что мы должны немного расширить наш пример, чтобы уловить все соответствующие биты:
#include <memory> #include <dispatch/dispatch.h> class MyClass : std::enable_shared_from_this<MyClass> { public: MyClass() { std::weak_ptr<MyClass> weak_this(shared_from_this()); dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ { auto strong_this = weak_this.lock(); strong_this->DoSomething(); }); } private: void DoSomething() { printf("Hi!\n"); } }; int main() { MyClass a; sleep(5); // To give the async op time to run }
Приведенный выше пример кода будет скомпилирован, но, к сожалению, он полон ошибок.
При первом запуске он будет прерван сterminating with uncaught exception of type std::__1::bad_weak_ptr: bad_weak_ptr
, брошенным из shared_from_this()
. Внимательно прочитав документацию, вы обнаружите, что:
Разрешено вызывать
shared_from_this
только для ранее совместно используемого объекта, то есть для объекта, управляемого std :: shared_ptr.
Хорошо, поэтому мы обновляем наш код, чтобы исправить это (обновленный код выделен жирным шрифтом):
#include <memory> #include <dispatch/dispatch.h> class MyClass : std::enable_shared_from_this<MyClass> { public: MyClass() { std::weak_ptr<MyClass> weak_this(shared_from_this()); dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ { auto strong_this = weak_this.lock(); strong_this->DoSomething(); }); } private: void DoSomething() { printf("Hi!\n"); } }; int main() { std::shared_ptr<MyClass> a(new MyClass); sleep(5); // To give the async op time to run }
Он компилируется и запускается, и по-прежнему вызывает исключение в том же месте. Внимательно прочтите дополнительную документацию:
Открытое наследование от
std::enable_shared_from_this<T>
предоставляет типуT
функцию-членshared_from_this
.
Я добавил акцент на «публично». Было бы очень хорошо, если бы компилятор реализовал эту маленькую ошибку ». Итак, мы исправляем это:
#include <memory> #include <dispatch/dispatch.h> class MyClass : public std::enable_shared_from_this<MyClass> { public: MyClass() { std::weak_ptr<MyClass> weak_this(shared_from_this()); dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ { auto strong_this = weak_this.lock(); strong_this->DoSomething(); }); } private: void DoSomething() { printf("Hi!\n"); } }; int main() { std::shared_ptr<MyClass> a(new MyClass); sleep(5); // To give the async op time to run }
Он компилируется, запускается и все еще генерирует исключение в том же месте. Читаю внимательно еще раз:
… В частности,
shared_from_this
нельзя вызывать в конструкторе
поскольку std::shared_ptr
, который собирается обернуть это, еще не построен.
#include <memory> #include <dispatch/dispatch.h> class MyClass : public std::enable_shared_from_this<MyClass> { public: void Register() { std::weak_ptr<MyClass> weak_this(shared_from_this()); dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ { auto strong_this = weak_this.lock(); strong_this->DoSomething(); }); } private: void DoSomething() { printf("Hi!\n"); } }; int main() { std::shared_ptr<MyClass> a(new MyClass); a->Register(); sleep(5); // To give the async op time to run }
который, наконец, компилирует и, выполняется до завершения. Обратите внимание: чтобы сделать наш класс безопасным для других пользователей, которые не знают о shared_ptr
требованиях, мы, вероятно, должны спрятать наш конструктор и предоставить нашим пользователям фабричный метод для работы с ним, который вынуждает вести себя правильно:
#include <memory> #include <dispatch/dispatch.h> class MyClass : public std::enable_shared_from_this<MyClass> { public: static std::shared_ptr<MyClass> Create() { return std::shared_ptr<MyClass>(new MyClass()); } void Register() { std::weak_ptr<MyClass> weak_this(shared_from_this()); dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ { auto strong_this = weak_this.lock(); strong_this->DoSomething(); }); } private: MyClass() {} void DoSomething() { printf("Hi!\n"); } }; int main() { auto a = MyClass::Create(); a->Register(); sleep(5); // To give the async op time to run }
Ура! Наконец-то безопасный перезвон в блоке (я думаю…).
Обратите внимание, что я использовал std::shared_ptr<MyClass>(new MyClass());
вместо std::make_shared<MyClass>();
. На это есть несколько причин:
- Поскольку я сделал свой конструктор закрытым, мне пришлось бы пройти через множество сверток, чтобы компилятор позволил мне компилировать.
- Даже если я получу его компиляцию,
std::make_shared
не всегда будет лучшим выбором при работе сstd::weak_ptrs
из-за потенциальных проблем с памятью.
У вас может возникнуть соблазн подумать, что вы можете избежать всех этих сверток с помощью вашего собственного класса-оболочки. Что-то с мьютексом, защищающим голый указатель на this
, который вы сбросите в своем деструкторе. В версиях, которые я видел, указатель this
в классе Objective C заключен в мьютекс. Вероятно, потому, что использование Objective C сохранило парадигму слабого / сильного «я», с которой они уже были знакомы. Что-то вроде:
#import <Foundation/Foundation.h> @class Wrapper; class MyClass { public: MyClass(); void DoSomething() { printf("Hi!\n"); } private: Wrapper *_wrapper; }; int main() { MyClass a; sleep(5); // To give the async op time to run } @interface Wrapper : NSObject { MyClass *_wrapped; } @end @implementation Wrapper - (instancetype)initWithMyClass:(MyClass *)wrapped { if ((self = [super init])) { _wrapped = wrapped; } return self; } - (void)reset { @synchronized(self) { _wrapped = nullptr; } } - (void)doSomething { @synchronized(self) { if (_wrapped) { _wrapped->DoSomething(); } } } @end MyClass::MyClass() { _wrapper = [[Wrapper alloc] initWithMyClass:this]; __weak Wrapper *weakWrapper = _wrapper; dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ { Wrapper *strongWrapper = weakWrapper; [strongWrapper doSomething]; }); } MyClass::~MyClass() { [_wrapper reset]; }
К сожалению, у вас всегда будет состояние гонки между моментом, когда ваш объект (MyClass) начнет уничтожаться, и когда вы можете обнулить этот голый указатель. Если MyClass имеет подкласс, возможно, он находится в частично разрушенном состоянии, если обратный вызов происходит между моментом запуска деструктора и вызовом [_wrapper reset]
. Пока вы гарантировали, что MyClass
не имеет подклассов и [_wrapper reset]
был первым, что вы вызывали в своем деструкторе, вы можете быть здесь в порядке.
Также посмотрите мой пост Objective C Encoding и обнаружите, что, смешивая C ++ / Objective C, как это было сделано выше, вы просто настраиваете себя на действительно неприятную генерацию метаданных Objective C в зависимости от того, насколько сложен
MyClass
.
Обратите внимание, что все вышеперечисленное в целом относится и к lambdas
. В lambdas
нет ничего волшебного, что могло бы вас спасти. Это не проблемы с лямбда / блоком, это проблемы с потоками. Блоки и лямбды просто облегчают их укусы.