Является ли поведение `new (this) MyClass ();` undefined после прямого вызова деструктора?

В этом моем вопросе @DeadMG говорит, что повторная инициализация класса с помощью указателя this является неопределенным поведением. Есть ли где-нибудь упоминание о нем в стандарте?

Пример:

#include <iostream>

class X{
  int _i;
public:  
  X() : _i(0) { std::cout << "X()\n"; }
  X(int i) : _i(i) { std::cout << "X(int)\n"; }

  ~X(){ std::cout << "~X()\n"; }

  void foo(){
    this->~X();
    new (this) X(5);
  }

  void print_i(){
    std::cout << _i << "\n";
  }
};

int main(){
  X x;
  x.foo();
  // mock random stack noise
  int noise[20];
  x.print_i();
}

Пример вывода на Ideone (я знаю, что UB также может быть «на вид правильным поведением»).
Обратите внимание, что Я не вызывал деструктор вне класса, чтобы не получить доступ к объекту, время жизни которого закончилось. Также обратите внимание, что @DeadMG говорит, что прямой вызов деструктора допустим, если он вызывается один раз для каждого конструктора.


person Xeo    schedule 03.06.2011    source источник
comment
Эта структура (вызов деструктора, а затем конструктора с размещением new) была довольно популярным способом реализации оператора присваивания до тех пор, пока не было обнаружено, что исключение небезопасно. Я не помню, чтобы кто-нибудь сказал, что это UB, если не считать исключения. Вероятно, есть случаи с виртуальными функциями и множественным наследованием, которые относятся к UB.   -  person AProgrammer    schedule 03.06.2011
comment
Если это UB, то только из-за использования this. Если это так, вы все равно можете обойти это, взяв копию this перед вызовом деструктора.   -  person Luc Danton    schedule 03.06.2011
comment
+1 Aпрограммист. На самом деле, стандарт C ++ 0x (FDIS) содержит пример ручного разрушения + конструкции размещения, так что это, вероятно, не так плохо. Это в §9.5 / 4 как способ изменить активный член объединения, когда некоторые из членов имеют нетривиальные конструкторы / деструкторы: u.m.~M(); new (&u.n) N; сейчас, в примерах это не делается из внутри метода, но я не знаю, имеет ли это значение.   -  person David Rodríguez - dribeas    schedule 03.06.2011
comment
@curiousguy Указатель на хранилище останется действующим. Я размышлял о том, существуют ли правила использования this в качестве ключевого слова, а не его конкретного значения. То есть, я думал о том, смогу ли я до получить доступ к значению (что, очевидно, правильно из-за других требований), будет ли вообще разрешено использование this?   -  person Luc Danton    schedule 30.09.2011
comment
@LucDanton this - это просто ключевое слово, используемое для получения значения неявного параметра нестатических функций-членов. Даже в программах, где this->~T();, delete this; или new (this) T; никогда не используются, this может относиться к объекту, который еще не полностью построен, в некоторых случаях, например, когда построение подобъекта даже не началось.   -  person curiousguy    schedule 30.09.2011
comment
@curiousguy Поэтому я использовал спекулятивную. Если возникла проблема, она могла возникнуть только из-за странного правила о this, потому что в противном случае код правильный. Я не говорил, что «использовать this неправильно».   -  person Luc Danton    schedule 30.09.2011
comment
@LucDanton Понятно. C ++ более регулярный и последовательный, чем вы могли себе представить! ;)   -  person curiousguy    schedule 30.09.2011


Ответы (2)


Это было бы нормально, если бы это не конфликтовало с раскруткой стека.

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

Проблема в том, что это небезопасное исключение. Что делать, если при вызове конструктора возникает исключение, стек разматывается и деструктор вызывается во второй раз?

{
   X x;
   x.foo(); // here ~X succeeds, then construction fails
} //then the destructor is invoked for the second time.

В частности, этот аспект будет неопределенным поведением.

person sharptooth    schedule 03.06.2011
comment
Ах, хорошее замечание о безопасности исключений. Можете ли вы придумать способ сделать это исключение безопасным? - person Xeo; 03.06.2011
comment
Что сделало бы функцию-член особенной для этого? (Никто, например, не притворяется, что delete this - UB.) - person AProgrammer; 03.06.2011
comment
@Xeo Оберните вызов конструктора в блок try ... catch и обработайте исключение таким образом, чтобы не вызывать UB. std::abort здесь приходит на ум. Или, поскольку это член, вызовите частный конструктор nothrow, который переводит объект в непригодное, но разрушаемое состояние, а затем (повторно) бросает. Пользователь не сможет увидеть объект в этом состоянии, и раскрутка стека будет работать. Но, пожалуйста, не рассматривайте это последнее решение как практическую вещь (в большинстве случаев). - person Luc Danton; 03.06.2011
comment
@Xeo: Ну, вам нужно временно отключить (или отключить эффект) разматывания стека. Не думаю, что это можно сделать каким-либо удобным и не подверженным ошибкам способом. - person sharptooth; 03.06.2011
comment
@Xeo: Я не думаю, что это можно сделать безопасным для исключений в общем случае. Проблема в том, что перед построением вы должны были выполнить деструкцию, и если конструктор не работает, состояние больше не является исходным. В классе с конструктором перемещения nothrow вы можете полностью избежать проблемы: создать локальную переменную, после создания переместить ее в *this. В этом случае он будет безопасным в отношении исключений (при условии, что деструктор также не работает). - person David Rodríguez - dribeas; 03.06.2011
comment
@David: Хорошая мысль о перемещении, это действительно сделало бы его безопасным в отношении исключений (предполагается, что перемещение не выполняется). - person Xeo; 03.06.2011
comment
@Xeo: ничего особенного ... безопасность исключений легко достигается в C ++ 03 путем конструирования в сторону и использования не бросающего _1 _... это просто еще один способ применения той же идиомы с другим синтаксисом. На самом деле, я все еще считаю, что идиома copy-and-swap лучше, так как вам все равно, выбрасывает деструктор или нет, уничтожение выполняется на копии после оригинала заменено местами , так что на этом этапе операция была полностью применена. Полагаю, семантика move - это наш новый золотой молоток - person David Rodríguez - dribeas; 03.06.2011
comment
Ах да, кстати, если вы реализуете конструктор перемещения, вам лучше не использовать его. Поступить иначе - это верный путь к катастрофе. Т.е. у вас могут возникнуть проблемы с некоторыми контейнерами STL: вектор будет перемещать объект из одного места в другое во время роста буфера, если одна из операций перемещения сработает, вектор останется в несовместимом состоянии с некоторыми из объекты в исходном векторе недействительны, а некоторые верны ... Я не рассматривал детали unordered_map, но предполагаю, что возникнет аналогичная проблема. - person David Rodríguez - dribeas; 03.06.2011

Помимо ответа @ sharptooth. Мне просто интересно еще 2 случая. По крайней мере, о них стоит упомянуть.

(1) Если foo() был вызван с использованием указателя на кучу, который был выделен с помощью malloc(). Конструктор не вызывается. Это может быть безопасно?

(2) Что делать, если производный класс вызывает foo(). Будет ли это хорошее поведение? например

struct Y : X {};  // especially when there is virtual ~X()
int main ()
{
  Y y;
  y.foo();
}
person iammilind    schedule 03.06.2011
comment
(1) предполагая, что вы имеете в виду указатель на память, выделенную с помощью malloc, тогда UB вызывает на нем член функции, если для создания объекта не использовалось размещение new. Затем пользователь обязан явным образом вызвать деструктор перед free загрузкой памяти. - person Luc Danton; 03.06.2011
comment
(2) поскольку деструктор X не виртуальный, this->~X() правильно уничтожает X подобъект Y, который затем создается заново. Если бы деструктор был виртуальным, foo мог бы использовать this->X::X(), чтобы сделать то же самое. - person Luc Danton; 03.06.2011
comment
(2) В конце функции (main) будет вызываться y.~Y(). После вызова y.foo() память по адресу y содержит X; вызов y.~Y() - это неопределенное поведение. (@Luc Danton Вы не можете восстановить базовый класс производного класса, если производный класс не перестанет существовать как таковой.) - person James Kanze; 03.06.2011
comment
@JamesKanze Спасибо за разъяснения; AProgrammer показал, как это действительно могло пойти не так. - person Luc Danton; 03.06.2011
comment
@James @Luc, значит, эти 2 случая делают этот сценарий UB, верно? - person iammilind; 03.06.2011
comment
Оба случая в ответе выше приводят к UB. В общем, этой идиомы следует избегать, потому что существует очень много разных способов, по которым она может в конечном итоге вызвать UB. - person James Kanze; 03.06.2011