Блочный синтаксис был введен 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>();. На это есть несколько причин:

  1. Поскольку я сделал свой конструктор закрытым, мне пришлось бы пройти через множество сверток, чтобы компилятор позволил мне компилировать.
  2. Даже если я получу его компиляцию, 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 нет ничего волшебного, что могло бы вас спасти. Это не проблемы с лямбда / блоком, это проблемы с потоками. Блоки и лямбды просто облегчают их укусы.