C ++: возвращение по ссылке и копирование конструкторов - PullRequest
19 голосов
/ 16 февраля 2010

Ссылки на C ++ сбивают меня с толку. :)

Основная идея заключается в том, что я пытаюсь вернуть объект из функции. Я хотел бы сделать это, не возвращая указатель (потому что тогда мне придется 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&, потому что это всегда приводит к уничтожению переменной до ее возвращения.
  • Конструктор копирования, похоже, включается только тогда, когда я возвращаю существующую переменную. При возврате переменной, созданной в строке возврата, она никогда не вызывается.
  • Когда я присваиваю результат существующей переменной, деструктор также всегда срабатывает, прежде чем возвращается значение. Кроме того, конструктор копирования не вызывается, но целевая переменная получает значения членов объекта, возвращаемые функцией.

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

Ответы [ 9 ]

16 голосов
/ 16 февраля 2010

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

Итог - если вам нужно вернуть значение, верните значение и не беспокойтесь о каких-либо «расходах».

14 голосов
/ 16 февраля 2010

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

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

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

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

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

MyClass x = fun();

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

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

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

MyClass& x = fun();

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

9 голосов
/ 16 февраля 2010

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

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

4 голосов
/ 16 февраля 2010

Если вы создаете объект, подобный этому:

MyClass foo(a, b, c);

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

Так что, если вы хотите вернуть объект вызывающей стороне, у вас есть только следующие варианты:

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

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

3 голосов
/ 16 февраля 2010

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

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

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

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

2 голосов
/ 16 февраля 2010

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

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

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

1 голос
/ 16 февраля 2010

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

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

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

  return 0;
}

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

Но подумайте, почему вы пытаетесь избежать копирования? Действительно ли «затраты» на создание копии влияют на вашу программу, или это случай преждевременной оптимизации?

0 голосов
/ 16 февраля 2010

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

0 голосов
/ 16 февраля 2010

Вы застряли с:

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

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

2) возврат копии объекта MyClass func () { вернуть MyClass (a, b, c); }

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

...