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&
потому что это всегда приводит к уничтожению переменной до ее возвращения. - Конструктор копирования, похоже, включается только тогда, когда я возвращаю существующую переменную. При возврате переменной, созданной в строке возврата, она никогда не вызывается.
- Когда я присваиваю результат существующей переменной, деструктор также всегда включается до того, как значение будет возвращено. Кроме того, не вызывается конструктор копирования, но целевая переменная получает значения членов объекта, возвращаемые функцией.
Эти результаты настолько противоречивы, что я чувствую себя совершенно сбитым с толку. Итак, что именно происходит здесь? Как правильно построить и вернуть объект из функции?
9 ответов
Лучший способ понять копирование в C++ - это НЕ пытаться создать искусственный пример и использовать его - компилятору разрешено как удалять, так и добавлять вызовы конструктора копирования, более или менее по своему усмотрению.
Итог - если вам нужно вернуть значение, верните значение и не беспокойтесь о каких-либо "расходах".
Рекомендуемое чтение: Эффективный C++ Скотта Мейерса. Вы найдете очень хорошее объяснение по этой теме (и многое другое) там.
Вкратце, если вы вернете по значению, конструктор копирования и деструктор будут задействованы по умолчанию (если компилятор не оптимизирует их - это то, что происходит в некоторых из ваших случаев).
Если вы возвращаете по ссылке (или указателю) переменную, которая является локальной (созданной в стеке), вы вызываете проблемы, потому что объект разрушается при возврате, поэтому в результате вы получаете висячую ссылку.
Канонический способ создать объект в функции и вернуть его по значению, например:
MyClass fun() {
return MyClass(a, b, c);
}
MyClass x = fun();
Если вы используете это, вам не нужно беспокоиться о проблемах владения, висячих ссылках и т. Д. И компилятор, скорее всего, оптимизирует дополнительные вызовы конструктора / деструктора копирования, поэтому вам также не нужно беспокоиться о производительности.
Можно вернуть по ссылке объект, созданный new
(т.е. в куче) - этот объект не будет уничтожен при возвращении из функции. Тем не менее, вы должны уничтожить его явно где-то позже, вызвав delete
,
Технически возможно также сохранить объект, возвращаемый по значению, в ссылке, например:
MyClass& x = fun();
Тем не менее, AFAIK нет особого смысла в этом. Тем более что эту ссылку можно легко передать другим частям программы, которые находятся за пределами текущей области; однако объект, на который ссылается x
это локальный объект, который будет уничтожен, как только вы покинете текущую область. Так что этот стиль может привести к неприятным ошибкам.
Прочитайте о RVO и NRVO (одним словом, эти два обозначают оптимизацию возвращаемого значения и именованный RVO, и представляют собой методы оптимизации, используемые компилятором для достижения того, чего вы пытаетесь достичь)
вы найдете много предметов здесь на стеке потока
Если вы создаете такой объект:
MyClass foo(a, b, c);
тогда он будет в стеке в кадре функции. Когда эта функция заканчивается, ее кадр выталкивается из стека, и все объекты в этом кадре разрушаются. Нет способа избежать этого.
Поэтому, если вы хотите вернуть объект вызывающей стороне, у вас есть только следующие варианты:
- Возврат по значению - требуется конструктор копирования (но вызов конструктора копирования может быть оптимизирован).
- Верните указатель и убедитесь, что вы используете умные указатели для работы с ним или осторожно удалите его самостоятельно, когда закончите с ним.
Попытка создать локальный объект и затем вернуть ссылку на эту локальную память вызывающему контексту не является последовательной - вызывающая область не может получить доступ к памяти, локальной для вызываемой области. Эта локальная память действительна только в течение срока действия функции, которой она принадлежит, или, иначе, пока выполнение остается в этой области. Вы должны понимать это, чтобы программировать на C++.
Единственный раз, когда имеет смысл возвращать ссылку, это если вы возвращаете ссылку на уже существующий объект. В качестве очевидного примера почти каждая функция-член iostream возвращает ссылку на iostream. Сам iostream существует до вызова любой из функций-членов и продолжает существовать после их вызова.
Стандарт допускает "разрешение копирования", что означает, что конструктор копирования не нужно вызывать при возврате объекта. Это происходит в двух формах: оптимизация возвращаемого значения имени (NRVO) и анонимная оптимизация возвращаемого значения (обычно это просто RVO).
Из того, что вы говорите, ваш компилятор реализует RVO, но не NRVO - это означает, что это, вероятно, несколько более старый компилятор. Большинство современных компиляторов реализуют оба. Несовпадающий dtor в этом случае означает, что это, вероятно, что-то вроде gcc 3.4 или около того - хотя я точно не помню версию, тогда была такая, в которой была такая ошибка. Конечно, также возможно, что ваш инструментарий не совсем правильный, поэтому используется ctor, который вы не использовали, и для этого объекта вызывается соответствующий dtor.
В конце концов, вы застряли на одном простом факте: если вам нужно вернуть объект, вам нужно вернуть объект. В частности, ссылка может дать доступ только (возможно, измененной версии) к существующему объекту, но этот объект также должен был быть создан в некоторой точке. Если вы можете изменить какой-либо существующий объект, не вызывая проблемы, это нормально, продолжайте и сделайте это. Если вам нужен новый объект, отличный от того, который у вас уже есть, продолжайте и сделайте это - предварительное создание объекта и передача ссылки на него может ускорить сам возврат, но не сэкономит время в целом. Создание объекта имеет примерно одинаковую стоимость, как внутри, так и вне функции. Любой достаточно современный компилятор будет включать RVO, так что вы не будете платить никаких дополнительных затрат за его создание в функции, а затем за ее возврат - компилятор просто автоматизирует выделение места для объекта, куда он будет возвращен, и получит функцию создайте его "на месте", где он все еще будет доступен после возврата функции.
По сути, возвращение ссылки имеет смысл, только если объект все еще существует после выхода из метода. Компилятор предупредит вас, если вы вернете ссылку на то, что уничтожается.
Возврат ссылки, а не объекта по значению, экономит копирование объекта, что может быть значительным.
Ссылки более безопасны, чем указатели, потому что они имеют различную символику, но за кулисами они являются указателями.
Одно из возможных решений, в зависимости от вашего варианта использования, состоит в том, чтобы создать объект по умолчанию вне функции, взять ссылку на него и инициализировать ссылочный объект внутри функции, например, так:
void initFoo(Foo& foo)
{
foo.setN(3);
foo.setBar("bar");
// ... etc ...
}
int main()
{
Foo foo;
initFoo(foo);
return 0;
}
Теперь это, конечно, не работает, если невозможно (или не имеет смысла) построить по умолчанию Foo
объект, а затем инициализировать его позже. Если это так, то единственный способ избежать копирования - это вернуть указатель на объект, выделенный из кучи.
Но тогда подумайте, почему вы пытаетесь избежать копирования в первую очередь. Действительно ли "затраты" на создание копии влияют на вашу программу, или это случай преждевременной оптимизации?
Не прямой ответ, а жизнеспособное предложение: вы также можете вернуть указатель, заключенный в auto_ptr или smart_ptr. Тогда вы будете контролировать, какие конструкторы и деструкторы будут вызываться и когда.
Вы застряли с:
1) возвращая указатель
MyClass* func(){ // некоторые вещи возвращают новый MyClass (a, b, c); }
2) возвращение копии объекта MyClass func(){ return MyClass(a,b,c); }
Возврат ссылки недопустим, поскольку объект должен быть уничтожен после выхода из области действия func, за исключением случаев, когда функция является членом класса, а ссылка - из переменной, которая является членом класса.