C ++ использует этот указатель в конструкторах - PullRequest
25 голосов
/ 25 марта 2010

В C++ во время конструктора класса я запустил новый поток с указателем this в качестве параметра, который будет широко использоваться в потоке (скажем, вызывая функции-члены). Это плохо делать? Почему и каковы последствия?

Мой процесс запуска потока находится в конце конструктора.

Ответы [ 8 ]

19 голосов
/ 25 марта 2010

В результате поток может запуститься, и код начнет выполнять еще не полностью инициализированный объект. Что само по себе достаточно плохо.

Если вы считаете, что «ну, это будет последнее предложение в конструкторе, оно будет примерно таким же построенным, как и полученное ...», подумайте еще раз: вы можете наследовать от этого класса, и производный объект будет не быть построенным.

Компилятор может захотеть поиграть с вашим кодом и решить, что он будет переупорядочивать инструкции, и он может фактически пройти указатель this перед выполнением любой другой части кода ... многопоточность сложна

4 голосов
/ 25 марта 2010

Основным следствием является то, что поток может начать работать (и использовать ваш указатель) до завершения конструктора, поэтому объект может не находиться в определенном / пригодном для использования состоянии. Аналогичным образом, в зависимости от того, как поток остановлен, он может продолжить работу после запуска деструктора, и поэтому объект снова может не быть в пригодном для использования состоянии.

Это особенно проблематично, если ваш класс является базовым классом, поскольку конструктор производного класса даже не запустится до тех пор, пока не выйдет ваш конструктор, а деструктор производного класса завершит работу до того, как начнется ваш. Кроме того, вызовы виртуальных функций не делают того, что вы думаете, до того, как производные классы созданы и после их разрушения: виртуальные вызовы «игнорируют» классы, часть объекта которых не существует.

Пример:

struct BaseThread {
    MyThread() {
        pthread_create(thread, attr, pthread_fn, static_cast<void*>(this));
    }
    virtual ~MyThread() {
        maybe stop thread somehow, reap it;
    }
    virtual void id() { std::cout << "base\n"; }
};

struct DerivedThread : BaseThread {
    virtual void id() { std::cout << "derived\n"; }
};

void* thread_fn(void* input) {
    (static_cast<BaseThread*>(input))->id();
    return 0;
}

Теперь, если вы создаете DerivedThread, это лучшая гонка между потоком, который его создает, и новым потоком, чтобы определить, какая версия id() вызывается. Может случиться так, что может случиться что-то худшее, вам нужно довольно внимательно взглянуть на API потоков и компилятор.

Обычный способ не беспокоиться об этом - просто дать вашему классу потока функцию start(), которую пользователь вызывает после ее создания.

1 голос
/ 25 марта 2010

По сути, вам нужно двухфазное построение : вы хотите запустить свой поток только после объект полностью построен. Джон Диблинг ответил на похожий (не дублирующий) вопрос вчера, в котором исчерпывающе обсуждалась двухфазная конструкция. Возможно, вы захотите взглянуть на это.

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

Таким образом, в конечном итоге, возможно, безопаснее всего запустить поток вручную:

class Thread { 
  public: 
    Thread();
    virtual ~Thread();
    void start();
    // ...
};

class MyThread : public Thread { 
  public:
    MyThread() : Thread() {}
    // ... 
};

void f()
{
  MyThread thrd;
  thrd.start();
  // ...
}
1 голос
/ 25 марта 2010

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

  1. Новый поток может попытаться получить доступ к объекту, прежде чем конструктор завершит его инициализацию. Вы можете обойти это, убедившись, что вся инициализация завершена, прежде чем запускать поток. Но что, если кто-то унаследует от вашего класса? Вы не можете контролировать, что будет делать их конструктор.
  2. Что произойдет, если ваш поток не запустится? На самом деле не существует чистого способа обработки ошибок в конструкторе. Вы можете выдать исключение, но это опасно, поскольку это означает, что деструктор вашего объекта не будет вызван. Если вы решите не генерировать исключение, то вы застряли при написании кода в различных методах, чтобы проверить, правильно ли инициализированы объекты.

Вообще говоря, если у вас есть сложная, подверженная ошибкам инициализация, то лучше делать это в методе, а не в конструкторе.

1 голос
/ 25 марта 2010

Это может быть потенциально опасно.

Во время создания базового класса любые вызовы виртуальных функций не будут отправляться для переопределений в более производных классах, которые еще не были полностью созданы; как только конструкция более производных классов изменится, это изменится.

Если поток, который вы запускаете, вызывает виртуальную функцию, и неясно, где это происходит в связи с завершением построения класса, тогда вы, вероятно, получите непредсказуемое поведение; возможно сбой.

Без виртуальных функций, если поток использует только методы и данные частей класса, которые были построены полностью, поведение, вероятно, будет предсказуемым.

1 голос
/ 25 марта 2010

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

Вы можете уменьшить риски, используя фабричный метод, который сначала создает объект, а затем запускает поток.

Но я думаю, что самый большой недостаток в дизайне заключается в том, что, по крайней мере, для меня конструктор, который делает больше, чем просто "конструирование", кажется довольно запутанным.

0 голосов
/ 11 февраля 2015

Некоторые люди считают, что вы не должны использовать указатель this в конструкторе, потому что объект еще не полностью сформирован. Однако вы можете использовать это в конструкторе (в {body} и даже в списке инициализации), если вы осторожны.

Вот то, что всегда работает: {body} конструктора (или функции, вызываемой из конструктора) может надежно обращаться к членам данных, объявленным в базовом классе, и / или членам данных, объявленным в собственном классе конструктора. Это потому, что все эти элементы данных гарантированно будут полностью построены к тому времени, как начинает выполняться {body} конструктора.

Вот то, что никогда не работает: {тело} конструктора (или функции, вызываемой из конструктора) не может перейти к производному классу, вызывая функцию виртуального члена, которая переопределяется в производном классе. Если вашей целью было получить переопределенную функцию в производном классе, вы не получите того, что хотите. Обратите внимание, что вы не получите переопределение в производном классе независимо от того, как вы вызываете виртуальную функцию-член: явно используя указатель this (например, this-> method ()), неявно используя указатель this (например, method ( )) или даже вызывая какую-то другую функцию, которая вызывает виртуальную функцию-член вашего объекта this. Суть в следующем: даже если вызывающая сторона создает объект производного класса, во время конструктора базового класса ваш объект еще не принадлежит этому производному классу. Вы были предупреждены.

Вот кое-что, что иногда работает: если вы передаете какой-либо элемент данных в этом объекте инициализатору другого элемента данных, вы должны убедиться, что другой элемент данных уже инициализирован. Хорошая новость заключается в том, что вы можете определить, был ли инициализирован (или нет) другой элемент данных, используя некоторые простые языковые правила, которые не зависят от конкретного компилятора, который вы используете. Плохая новость заключается в том, что вы должны знать эти языковые правила (например, субобъекты базового класса инициализируются первыми (посмотрите порядок, если у вас множественное и / или виртуальное наследование!), Затем элементы данных, определенные в классе, инициализируются в порядок, в котором они появляются в объявлении класса). Если вы не знаете этих правил, не передавайте ни одного члена данных из объекта this (независимо от того, используете ли вы явно ключевое слово this) инициализатору любого другого члена данных! И если вы знаете правила, будьте осторожны.

0 голосов
/ 25 марта 2010

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...