Передача `this` до завершения базовых конструкторов: UB или просто опасно?

Рассмотрим этот самый маленький пример (я мог придумать):

struct Bar;

struct Foo {
  Bar* const b;
  Foo(Bar* b) : b(b) {}
};

struct Bar {
  Foo* const f;
  Bar(Foo* f) : f(f) {}
};

struct Baz : Bar {
  Baz() : Bar(new Foo(this)) {}
};

При передаче this ctor Foo ничего в иерархии Baz не создается, но ни Foo, ни Bar не делают ничего проблемного с полученными указателями.

Теперь вопрос в том, просто ли опасно отдавать this таким образом или это неопределенное поведение?

Вопрос 2: Что, если Foo::Foo(Bar*) было Foo::Foo(Bar&) с той же семантикой? Мне пришлось бы передать *this, но в этом случае оператор deref ничего не сделал бы.


person bitmask    schedule 14.11.2011    source источник
comment
+1 Хороший вопрос, вы явно думали об этом перед публикацией.   -  person wilhelmtell    schedule 15.11.2011
comment
Прямо сейчас я не буду обвинять вас в том, что вы просматриваете веб-страницы с помощью Opera. Но я оставляю за собой право сделать это в будущем, если мои адвокаты сочтут это необходимым.   -  person wilhelmtell    schedule 15.11.2011


Ответы (4)


Ответ на этот вопрос содержится непосредственно в стандарте С++ 3.8/5:

До того, как время жизни объекта началось, но после того, как память, которую будет занимать объект, была выделена, или после того, как время жизни объекта закончилось и до того, как память, которую занимает объект, будет повторно использована или освобождена, любой указатель, который ссылается на память место, где объект будет или находился, может быть использовано, но только ограниченным образом. Для строящегося или разрушаемого объекта см. 12.7. В противном случае такой указатель ссылается на выделенное хранилище (3.7.4.2), и использование указателя, как если бы указатель был типа void*, четко определен. Такой указатель может быть разыменован, но результирующее значение lvalue может использоваться только ограниченными способами, как описано ниже. Программа имеет неопределенное поведение, если:

  • объект будет или был типа класса с нетривиальным деструктором, а указатель используется как операнд выражения удаления,
  • the pointer is used to access a non-static data member or call a non-static member function of the object, or
  • the pointer is implicitly converted (4.10) to a pointer to a base class type, or
  • указатель используется в качестве операнда static_cast (5.2.9) (за исключением случаев, когда преобразование выполняется в void* или в void* и впоследствии в char* или unsigned char*), или
  • указатель используется как операнд dynamic_cast (5.2.7).

Кроме того, в 12.7/3:

Для явного или неявного преобразования указателя (glvalue), ссылающегося на объект класса X, в указатель (ссылку) на прямой или косвенный базовый класс B X, построение X и построение всех его прямых или косвенных оснований которые прямо или косвенно происходят от B, должны быть запущены, и уничтожение этих классов не должно быть завершено, в противном случае преобразование приводит к неопределенному поведению.

person Gene Bushuyev    schedule 14.11.2011

Это не УБ. Объект может быть еще не инициализирован должным образом (поэтому его использование сразу может оказаться невозможным), но сохранение указателя на потом вполне допустимо.

Мне пришлось бы передать *this, но в этом случае оператор deref ничего не сделал бы.

Конечно, это было бы разыменованием указателя. Помните, что инициализация — это не то же самое, что выделение — когда конструктор запускается, объект уже правильно выделен (иначе вы не смогли бы его инициализировать) — т. е. он существует, но находится в неопределенном состоянии, пока не завершится его конструктор.

person Cat Plus Plus    schedule 14.11.2011
comment
В T* tp = ...; T& tr = *tp; оператор * ничего не делает, верно? В том смысле, что это не отражается в сгенерированном коде. - person bitmask; 14.11.2011
comment
@bitmask разыменовывает указатель. Вряд ли это можно назвать ничем. На любом приличном компиляторе сгенерированный код будет сильно отличаться от того, что вы написали, поэтому вам, вероятно, следует игнорировать это. - person R. Martinho Fernandes; 14.11.2011
comment
@Р. Мартиньо Фернандеш: Подождите секунду: T& — это просто указатель на T с синтаксическим сахаром, верно? Что мне не хватает? - person bitmask; 14.11.2011
comment
T& — это ссылка на T, T* — это указатель на T. Они совсем не одинаковы (нет, правда, если бы были, на кой черт нам нужны они оба?). Ссылка не является указателем. Основные отличия заключаются в том, что ссылка не может быть переустановлена ​​и не может быть недействительной. - person R. Martinho Fernandes; 14.11.2011
comment
@bitmask: Нет, T& — это ссылка. - person Cat Plus Plus; 14.11.2011
comment
Я говорю о логической перспективе. Насколько я знаю, нет компилятора, который бы не реализовывал ссылки с указателями. Так что для всего, что имеет значение, ссылка является константным указателем. Конечно, ссылки синтаксически отличаются от указателей, но это всего лишь синтаксический сахар. Итак, все, что я хочу сказать, это то, что * в приведенном выше примере — это просто... преобразование типа. - person bitmask; 14.11.2011
comment
@bitmask в некоторых ситуациях компиляторы могут реализовать ссылки как вообще ничего (подумайте о встраивании и локальных переменных). Но в любом случае ссылка не похожа на константный указатель. Ссылка никогда не бывает нулевой, чего нельзя сказать о константном указателе. Ссылка имеет семантическое значение, и это делает ее гораздо большим, чем синтаксический сахар. И я до сих пор не понимаю, почему преобразование типа ничего. - person R. Martinho Fernandes; 14.11.2011
comment
Ссылка - это псевдоним, в данном случае *tp, стандарт не предписывает механизм или даже то, что этот псевдоним действительно существует в сгенерированном коде. Это также не UB для разыменования неинициализированного указателя, если он не используется или находится в неоцененном контексте. Таким образом, T& tr = *tp; не вводит UB, пока не используется tp. - person Gene Bushuyev; 14.11.2011
comment
@bitmask: неважно, как это реализуют компиляторы. Ссылки имеют другую семантику, чем указатели. Особенно, когда вы рассматриваете ссылки rvalue, которые не имеют прямого аналога в мире указателей. - person Cat Plus Plus; 14.11.2011
comment
@GeneBushuyev: На самом деле это предмет разногласий; см. примечания здесь. - person GManNickG; 15.11.2011
comment
@GMan - да, я помню давние обсуждения, у меня сложилось впечатление, что это давно решено. В любом случае FDIS имеет следующую формулировку, применимую к этому обсуждению в 5.3.1/1: Примечание: указатель на неполный тип (отличный от cv void) может быть разыменован. Полученное таким образом значение lvalue можно использовать ограниченным образом (например, для инициализации ссылки); - person Gene Bushuyev; 15.11.2011
comment
@Gene: неполные типы отличаются от неинициализированных (или нулевых) значений указателя. - person GManNickG; 15.11.2011
comment
@GMan, но здесь this является указателем на неполный тип: 9.2/2 В спецификации члена класса класс считается полным в телах функций, аргументах по умолчанию, спецификациях исключений и инициализаторах скобок или равенства для нестатические элементы данных (включая такие вещи во вложенных классах). В противном случае он считается неполным в пределах своей собственной спецификации члена класса. - person Gene Bushuyev; 15.11.2011
comment
@GeneBushuyev: Чтобы было ясно, я говорю об этом утверждении: это также не UB для разыменования неинициализированного указателя, если он не используется или находится в неоцененном контексте. Если tp не инициализирован (T* tp;) или имеет значение null (T* tp = 0;), это поведение undefined. И снова слово «неполный» не имеет ничего общего с значениями, только с типами. - person GManNickG; 15.11.2011
comment
@GMan - я мог читать эти комментарии поверхностно, и обсуждение переключилось на другие вопросы. Я смотрел на исходный Baz() : Bar(new Foo(this)), предполагая, что OP спрашивал, что произошло бы, если бы Foo инициализировал ссылку вместо копирования указателя. P.S. да, типы могут быть неполными в одном месте и полными в другом, так вот указатель. - person Gene Bushuyev; 15.11.2011
comment
@Р. Мартиньо Фернандеш, вы совершенно не правы. Ссылка может и будет указывать на NULL. Разница лишь в том, что трудно понять, куда он указывает. int* pInt = NULL; int& refInt = *pInt; Компиляторы могут реализовать практически все, что угодно, включая доступ к указателям. Разница чисто психологическая - большинство людей до смерти напуганы словесным адресом. - person Agent_L; 15.01.2012
comment
@Р. Мартиньо Фернандес: также &(*(this)) вообще не разыменовывается, это просто нулевая операция. - person Agent_L; 15.01.2012
comment
@Agent_L: Мне, наверное, даже не стоило поправлять тебя. Но я все равно буду. В C++ нет нулевых ссылок. Это не психология, это факт ((...) Ссылка должна быть инициализирована для ссылки на допустимый объект или функцию [Примечание: в частности, нулевая ссылка не может существовать в четко определенной программе (... ) — из стандарта C++ §8.3.2). В приведенном вами примере компилятору разрешено создавать программу, которая выводит "This is not a well-defined program. It helps to learn C++ before writing it." - person R. Martinho Fernandes; 15.01.2012
comment
@Agent_L: И &(*(this)) разыменовывает указатель и применяет к нему оператор &. I может быть или не быть операцией идентификации (ideone.com/mMGzF), но она всегда включает разыменование . - person R. Martinho Fernandes; 15.01.2012
comment
@Р. Martinho Fernandes Не инициируй ссылки на NULL - это как раз психологическая разница. Я говорю о технических здесь. (Если бы все программы были четко определены, у нас не было бы ошибок) - person Agent_L; 16.01.2012
comment
@Р. Martinho Fernandes Есть 2 определения того, что означает разыменование. Один из них синтаксический, и вы правы здесь. Другой функциональный: доступ к тому, на что указывает указатель. Написание просто (*this) вообще не имеет доступа. Вы меня поняли, я должен был написать более сложный пример с & в аргументе функции. Но точка непочтительности здесь остается в силе. - person Agent_L; 16.01.2012

Поведение не является неопределенным и не обязательно опасным.

Ни Foo, ни Bar не делают никаких проблем с полученными указателями.

Это ключ: вам просто нужно знать, что объект, на который указывает указатель, еще не полностью построен.

Что, если бы Foo::Foo(Bar*) было Foo::Foo(Bar&) с той же семантикой?

На самом деле между ними нет никакой разницы, если говорить об опасности или определенности.

person James McNellis    schedule 14.11.2011
comment
Меня беспокоит следующее: если у кого-то появится отличная идея получить доступ, например. член экземпляра uncreated, все сломается. Поэтому мне было интересно, что стандарт должен сказать о раздаче неинициализированных объектов (Примечание: меня больше всего беспокоит то, что они раздаются ДО создания базовых классов, поэтому НИКАКОЙ ctor еще не выполнялся!). Вопрос со ссылками имеет такое обоснование: ничто не может пойти не так, если никто не удалит указатель. - person bitmask; 14.11.2011
comment
Я не уверен, что правильно говорить, что это не опасно. Я знаю, опасно это или нет, зависит от того, что конструктор делает с указателем. Если конструктор просто хранит указатель, то все в порядке. Эта ситуация показывает, что автор Foo должен четко указать, какие предположения делает конструктор относительно принимаемого им указателя. Тогда вызывающий исходит из этого. - person wilhelmtell; 15.11.2011
comment
@wilhelmtell: Вот почему я не сказал, что это не опасно; Я сказал, что это не обязательно опасно. ;-) Лично я старался бы избегать этого, если только вы не контролируете весь рассматриваемый код (т. е. не передаете указатели на частично сконструированные объекты коду, который вы не контролируете). Но я мог бы представить себе сценарий, в котором это потребуется, и в этом случае документация является ключевой. - person James McNellis; 15.11.2011

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

Программа имеет неопределенное поведение, если:

[...]

  • указатель неявно преобразуется в указатель на тип базового класса или

[...]

В вашем примере тип указателя в параметре базового класса имеет тип базового класса, поэтому указатель this производного класса должен быть неявно преобразован в него. Что является неопределенным поведением в соответствии с вышеизложенным. Но... чтобы вызвать конструктор базового класса, компилятор должен неявно преобразовать адрес в указатель типа на базовый класс. Так что должны быть какие-то исключения.

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

class L;
class VB {};
class R : virtual VB { public: R( L* ); }
class L { L( char const* p ); };
class D : private virtual L, private virtual R { D(); }
D::D( char const* p ) : L( p ), R( this ) {}

Почему у компилятора были проблемы здесь, я не знаю. Он смог правильно преобразовать указатель, чтобы передать его как указатель this конструктору L, но не сделал этого правильно при передаче его в R.

В этом случае обходной путь состоял в том, чтобы предоставить класс-оболочку для L с функцией-членом, которая возвращала указатель, например:

class LW : public L
{
public:
    LW( char const* p ) : L( p ) {}
    L* getAddress() { return this; }
};

D::D( char const* p ) : L( p ), R( this->getAddress(); ) {}

Результатом всего этого является то, что я не могу дать вам однозначный ответ, потому что не уверен, что имели в виду авторы стандарта. С другой стороны, я действительно видел случаи, когда это не срабатывало (и не так давно).

person James Kanze    schedule 14.11.2011
comment
Спасибо за фактическое цитирование стандарта :) В вашем случае речь идет о виртуальном наследовании, которое может быть причиной ошибки компилятора. Во всяком случае: приведение к базовому классу уже является UB, что означает, что мой пример определенно является UB. Интересно, почему это не определено, даже если виртуальное наследование не задействовано, для меня это не имеет смысла. - person bitmask; 15.11.2011
comment
@bitmask Согласно стандарту, моя проблема была не из-за ошибки компилятора; код имеет неопределенное поведение. Почему это не определено, это другой вопрос: в какой-то произвольной функции я могу понять проблемы (хотя они не являются непреодолимыми); в конструкторе компилятор должен иметь возможность выполнить преобразование, чтобы передать указатель конструкторам базового класса, так почему бы не разрешить это в противном случае. - person James Kanze; 15.11.2011