Подтвердить что ты не робот

С++: возврат по ссылке и копии конструкторов

Ссылки на С++ меня озадачивают.:)

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

Итак, в целом, вот варианты для этого, которые я нашел:

  • Возвращаемый тип функции может быть либо самим классом (MyClass fun() { ... }), либо ссылкой на класс (MyClass& fun() { ... }).
  • Функция может либо построить переменную в строке возврата (return MyClass(a,b,c);), либо вернуть существующую переменную (MyClass x(a,b,c); return x;).
  • Код, который принимает переменную, также может иметь переменную любого типа: (MyClass x = fun(); или MyClass& x = fun();)
  • Код, который получает переменную, может либо создать новую переменную "на лету" (MyClass x = fun();), либо назначить ее существующей переменной (MyClass x; x = fun();)

И некоторые мысли по этому поводу:

  • Кажется, плохая идея иметь тип возвращаемого значения MyClass&, потому что это всегда приводит к уничтожению переменной до ее возвращения.
  • Конструктор копирования только кажется вовлеченным, когда я возвращаю существующую переменную. При возврате переменной, построенной в строке возврата, она никогда не вызывается.
  • Когда я присваиваю результат существующей переменной, деструктор также всегда запускается до того, как возвращается значение. Кроме того, конструктор копирования не вызван, но целевая переменная получает значения членов объекта, возвращаемого функцией.

Эти результаты настолько непоследовательны, что я чувствую себя совершенно смущенным. Итак, что ТОЧНО происходит здесь? Как правильно построить и вернуть объект из функции?

4b9b3361

Ответ 1

Лучший способ понять копирование на С++ - это часто НЕ пытаться создать искусственный пример и измерить его - компилятору разрешено как удалять, так и добавлять вызовы конструктора копирования, более или менее по своему усмотрению.

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

Ответ 2

Рекомендуемое чтение: Эффективный С++ от Scott Meyers. Вы найдете очень хорошее объяснение по этой теме (и многое другое) там.

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

Если вы возвращаете по ссылке (или указателю) переменную, которая является локальной (построенной в стеке), вы вызываете проблему, потому что объект разрушается при возврате, поэтому в результате у вас есть оборванная ссылка.

Канонический способ построения объекта в функции и возврата его по значению, например:

MyClass fun() {
    return MyClass(a, b, c);
}

MyClass x = fun();

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

Можно вернуть по ссылке объект, построенный с помощью new (т.е. в куче) - этот объект не будет уничтожен при возврате из функции. Однако вы должны уничтожить его явно где-то позже, вызвав delete.

Также технически возможно сохранить объект, возвращаемый значением в ссылке, например:

MyClass& x = fun();

Однако AFAIK не имеет большого смысла в этом. Тем более, что можно легко передать эту ссылку другим частям программы, которые находятся за пределами текущей области; однако объект, на который ссылается x, является локальным объектом, который будет уничтожен, как только вы покинете текущую область. Таким образом, этот стиль может привести к неприятным ошибкам.

Ответ 3

читайте о RVO и NRVO (одним словом, эти две позиции означают Оптимизацию возвращаемого значения и Именованное RVO и являются методами оптимизации, используемыми компилятором делать то, что вы пытаетесь достичь)

вы найдете много предметов здесь, в stackoverflow

Ответ 4

О единственном времени, когда имеет смысл возвращать ссылку, - это возврат ссылки на ранее существовавший объект. Для очевидного примера почти каждая функция члена iostream возвращает ссылку на iostream. Сам iostream существует до того, как вызывается любая из функций-членов и продолжает существовать после их вызова.

Стандарт позволяет "copy elision", что означает, что конструктор копирования не нужно вызывать, когда вы возвращаете объект. Это происходит в двух формах: Оптимизация возвращаемого значения имени (NRVO) и анонимная оптимизация возвращаемого значения (обычно только RVO).

Из того, что вы говорите, ваш компилятор реализует RVO, но не NRVO, что означает, что он, вероятно, несколько старше компилятор. Большинство современных компиляторов реализуют оба. Несоответствующий dtor в этом случае означает, что он, вероятно, что-то вроде gcc 3.4 или около того - хотя я точно не помню версию, тогда был один, у которого была ошибка. Конечно, также возможно, что ваша аппаратура не совсем правильная, поэтому используется ctor, в котором вы не использовали инструмент, и для этого объекта вызывается соответствующий dtor.

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

Ответ 5

Если вы создаете такой объект:

MyClass foo(a, b, c);

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

Итак, если вы хотите вернуть объект вызывающему, вы можете выбрать только следующие параметры:

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

Попытка построить локальный объект и затем вернуть ссылку на эту локальную память на вызывающий контекст не является когерентной - область вызова не может получить доступ к памяти, которая является локальной для вызываемой области. Эта локальная память действительна только на протяжении всей функции, которая ее владеет, или, иначе, пока выполнение остается в этой области. Вы должны понимать это для программирования на С++.

Ответ 6

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

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

Ссылки более безопасны, чем указатели, потому что они имеют разные симматики, но за кулисами они являются указателями.

Ответ 7

Одно потенциальное решение, в зависимости от вашего варианта использования, состоит в том, чтобы по умолчанию построить объект вне функции, взять ссылку на него и инициализировать ссылочный объект внутри функции, например:

void initFoo(Foo& foo) 
{
  foo.setN(3);
  foo.setBar("bar");
  // ... etc ...
}

int main() 
{
  Foo foo;
  initFoo(foo);

  return 0;
}

Теперь это, конечно, не работает, если невозможно (или не имеет смысла) по умолчанию построить объект Foo, а затем инициализировать его позже. Если это так, то ваша единственная реальная возможность избежать копирования-построения - вернуть указатель на объект, выделенный для кучи.

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

Ответ 8

У вас тоже есть:

1), возвращая указатель

MyClass * func() { // некоторые stuf вернуть новый MyClass (a, b, c); }

2), возвращая копию объекта MyClass func() { return MyClass (a, b, c); }

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

Ответ 9

Не прямой ответ, но жизнеспособное предложение: вы также можете вернуть указатель, завернутый в auto_ptr или smart_ptr. Тогда вы будете контролировать, какие конструкторы и деструкторы вызываются и когда.