Построить объект с собой в качестве ссылки? - PullRequest
8 голосов
/ 06 декабря 2010

Я только что понял, что эта программа компилируется и запускается (gcc версия 4.4.5 / Ubuntu):

#include <iostream>
using namespace std;

class Test
{
public:
  // copyconstructor
  Test(const Test& other);
};

Test::Test(const Test& other)
{
  if (this == &other)
    cout << "copying myself" << endl;
  else
    cout << "copying something else" << endl;
}

int main(int argv, char** argc)
{
  Test a(a);              // compiles, runs and prints "copying myself"
  Test *b = new Test(*b); // compiles, runs and prints "copying something else"
}

Интересно, почему на земле это даже компилируется.Я предполагаю, что (как и в Java) аргументы оцениваются до вызова метода / конструктора, поэтому я подозреваю, что этот случай должен быть охвачен каким-то «особым случаем» в спецификации языка?

Вопросы:

  1. Может ли кто-нибудь объяснить это (желательно, ссылаясь на спецификацию)?
  2. Каково обоснование для этого?
  3. Это стандарт C ++или это зависит от gcc?

РЕДАКТИРОВАТЬ 1: Я только что понял, что я могу даже написать int i = i;

РЕДАКТИРОВАТЬ 2: Даже с -Wall и -pedantic компилятор не жалуется на Test a(a);.

РЕДАКТИРОВАТЬ 3: Если я добавлю метод

Test method(Test& t)
{
  cout << "in some" << endl;
  return t;
}

, я даже могу сделать Test a(method(a)); без каких-либо предупреждений.

Ответы [ 6 ]

8 голосов
/ 06 декабря 2010

Причина, по которой это «разрешено», заключается в том, что правила говорят, что область идентификаторов начинается сразу после идентификатора. В случае

int i = i;

RHS я "после" LHS я, так что я в области. Это не всегда плохо:

void *p = (void*)&p; // p contains its own address

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

struct List { List *next; List(List &n) { next = &n; } };

если вы видите, что аргумент просто адресован, его значение не используется. В этом случае самоссылка может иметь смысл: хвост списка задается самоссылкой. Действительно, если вы измените тип «next» на ссылку, у вас будет мало выбора, поскольку вы не можете легко использовать NULL, как вы могли бы использовать для указателя.

Как обычно, вопрос задом наперед. Вопрос не в том, почему инициализация переменной может ссылаться на себя, а в том, почему она не может ссылаться вперед. [В Феликсе это возможно]. В частности, для типов, в отличие от переменных, отсутствие возможности пересылки ссылок крайне затруднено, так как не позволяет определить рекурсивные типы, кроме использования неполных типов, что достаточно в C, но не в C ++ из-за существования шаблоны.

3 голосов
/ 06 декабря 2010

Первый случай (возможно) покрывается 3,8 / 6:

до начала срока службы объекта, но после того, как хранилище, которое будет занимать объект, было выделено или, после срока службыобъекта закончился, и до того, как хранилище, в котором занятый объект, будет повторно использовано или освобождено, может использоваться любое значение l, которое относится к исходному объекту, но только ограниченным образом.Такое lvalue относится к выделенному хранилищу (3.7.3.2), и использование свойств lvalue, которые не зависят от его значения, четко определено.

Поскольку все, что вы используете, aother, что связано с a) до того, как начало его жизни является адресом, я думаю, что вы хорошо: прочитайте остальную часть этого параграфа для подробных правил.

Осторожнохотя в этом 8.3.2 / 4 говорится: «Ссылка должна быть инициализирована для ссылки на действительный объект или функцию».Существует некоторый вопрос (как отчет о дефектах по стандарту), что означает «действительный» в этом контексте, поэтому, возможно, вы не можете связать параметр other с неструктурированным (и, следовательно, «недействительным»?) a.

Итак, я не уверен, что стандарт на самом деле здесь говорит - я могу использовать lvalue, но, возможно, не привязывать его к ссылке, в этом случае a не хорошо, при передаче указателя на a все будет в порядке, если только он используется способами, разрешенными 3.8 / 5.

В случае b, вы используете значение до того, какинициализирован (потому что вы разыменовываете его, а также потому, что даже если вы зашли так далеко, &other будет значением b).Это явно нехорошо.

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

3 голосов
/ 06 декабря 2010

Второй, где вы используете new, на самом деле легче понять; то, что вы вызываете, точно так же, как:

Test *b;
b = new Test(*b);

и вы фактически выполняете недопустимую разыменование. Попробуйте добавить << &other << к вашим cout строкам в конструкторе и сделать это

Test *b = (Test *)0xFOOD1E44BADD1E5;

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

Во-первых, думайте об этом как о месте new. Test a - это локальная переменная, а не указатель, она живет в стеке и, следовательно, ее расположение в памяти всегда четко определено - это очень сильно отличается от указателя, Test *b, который, если явно не инициализирован в какое-либо действительное местоположение, будет зависать .

Если вы напишите свой первый экземпляр как:

Test a(*(&a));

становится понятнее, что вы там вызываете.

Я не знаю способа заставить компилятор запретить (или даже предупредить) о такого рода самоинициализации из ниоткуда через конструктор копирования.

3 голосов
/ 06 декабря 2010

Я понятия не имею, как это связано со спецификацией, но вот как я это вижу:

Когда вы делаете Test a(a);, это выделяет место для a в стеке.Поэтому расположение a в памяти известно компилятору в начале main.Когда вызывается конструктор (перед этим, конечно, выделяется память), ему передается правильный указатель this, потому что он известен.

Когда вы делаете Test *b = new Test(*b);, вам нужно думать о нем какдва шага.Сначала объект выделяется и создается, а затем указатель на него присваивается b.Причина, по которой вы получаете сообщение, которое вы получаете, состоит в том, что вы по существу передаете неинициализированный указатель на конструктор и сравниваете его с фактическим this указателем объекта (который в конечном итоге будет присвоен b, нодо выхода из конструктора).

2 голосов
/ 06 декабря 2010

Если вы повысите уровень предупреждений, ваш компилятор, вероятно, предупредит вас об использовании неинициализированного содержимого.UB не требует диагностики, многие вещи, которые "явно" неправильны, могут компилироваться.

0 голосов
/ 06 декабря 2010

Я не знаю ссылку на спецификацию, но я знаю, что доступ к неинициализированному указателю всегда приводит к неопределенному поведению.

Когда я компилирую ваш код в Visual C ++, я получаю:

test.cpp (20): предупреждение C4700: используется неинициализированная локальная переменная 'b'

...