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

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

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


person Vilx-    schedule 16.02.2010    source источник
comment
Причина, по которой конструктор копирования не вызывается во втором случае, называется оптимизацией возвращаемого значения или RVO. Компилятору разрешено опускать временную копию +, даже если это изменяет поведение программы.   -  person GManNickG    schedule 16.02.2010
comment
@Vilx- А как насчет возврата к существующей проблеме с переменной? Я столкнулся с этим, но я не мог понять, почему деструктор запускает конструктор без копирования. Вы когда-нибудь выясняли, в чем причина этого?   -  person Arthur Nunes    schedule 23.10.2015
comment
@ArthurNunes - Извините, нет, я мало работал с C++ и не знаю ответа. Думаю, я забросил этот проект или переписал его по-другому. (это было 5 лет назад, вы знаете).   -  person Vilx-    schedule 23.10.2015


Ответы (9)


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

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

person Community    schedule 16.02.2010
comment
Хорошо, я принимаю это. Тем не менее я должен задаться вопросом - а как насчет объектов, которые не должны быть клонированы? Как сетевой сокет, как в моем случае. Должен ли я тогда возвращать указатель? Или я могу зависеть от оптимизированного конструктора копирования? - person Vilx-; 16.02.2010
comment
@Vilx- это еще один вопрос. И ответ в том, что такие вещи не должны копироваться или быть копируемыми. - person ; 16.02.2010
comment
@Vilx: это исправлено в следующем стандарте C++ с семантикой move. Вместо того, чтобы делать копию, а затем терять ее для передачи объекта, вы просто перемещаете внутренние данные; никогда не существует более одной копии. - person GManNickG; 16.02.2010

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

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

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

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

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

MyClass x = fun();

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

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

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

MyClass& x = fun();

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

person Péter Török    schedule 16.02.2010
comment
как насчет использования const MyClass& x = fun(); переменная не уничтожается при возврате/выходе из области видимости. Это нормально? - person Mannaggia; 28.02.2016

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

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

person f4.    schedule 16.02.2010

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

MyClass foo(a, b, c);

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

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

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

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

person Tom    schedule 16.02.2010
comment
Да, но оптимизированный конструктор копирования меня смущал. Объект был бы довольно... грязным для клонирования, поскольку он обертывает сетевой сокет и все такое. - person Vilx-; 16.02.2010
comment
Не обязательно - сокет - это просто файловый дескриптор, поэтому, когда вы клонируете объект, вы можете "клонировать" сетевой сокет, просто скопировав FD. - person Tom; 18.02.2010

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

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

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

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

person Jerry Coffin    schedule 16.02.2010
comment
На самом деле это Visual Studio 2008. :) Хотя это и отладочная компиляция, многие оптимизации отключены. - person Vilx-; 16.02.2010
comment
Ах, это в любом случае объясняет отсутствие NRVO. Не уверен насчет непревзойденного dtor - у вас может быть ошибка компилятора, но неинструментальный ctor, вероятно, более вероятен. - person Jerry Coffin; 16.02.2010

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

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

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

person user231967    schedule 16.02.2010
comment
Компилятор может предупредить вас (и обычно делает), но сложные случаи не обнаруживаются, и не все компиляторы могут анализировать их в первую очередь. - person Tronic; 16.02.2010
comment
Какой компилятор здесь ошибается? Пример? - person user231967; 16.02.2010
comment
@nodan: Visual Studio 2010 не выдает предупреждения: int& f(bool f, int& x) { int y = 0; return f ? x : y; } int main() { int x; int& i = f(false, x); }. - person GManNickG; 16.02.2010
comment
Возврат по значению может быть таким же дешевым, если компилятор применяет N/RVO. и Ultra Pedant Mode: все известные компиляторы реализуют ссылки, используя те же конструкции машинного кода, что и указатели, но стандарт не требует этого, поэтому кто-то может использовать другой метод, если захочет (что, вероятно, было бы очень глупо). , но тем не менее возможно). - person underscore_d; 16.02.2016

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

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

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

  return 0;
}

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

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

person Tyler McHenry    schedule 16.02.2010
comment
На самом деле это тот случай, когда мне действительно нужно написать чертов конструктор копирования. Меньше кода, меньше ошибок. :П - person Vilx-; 16.02.2010
comment
@Vilx: В этом почти никогда не должно быть необходимости. Ваши члены должны копировать себя. - person GManNickG; 16.02.2010
comment
Если нет какой-то динамически выделяемой памяти, которая освобождается при уничтожении. И я не думаю, что это очень редко, не так ли? - person Vilx-; 17.02.2010
comment
@Vilx GMan подразумевает, что вы должны использовать интеллектуальные указатели (которые автоматически освобождаются при уничтожении) вместо необработанных указателей, когда это необходимо. - person Tyler McHenry; 17.02.2010

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

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

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

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

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

person no_ripcord    schedule 16.02.2010

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

person florin    schedule 16.02.2010