Как «вернуть объект» в C ++?

167

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

В Java я всегда могу вернуть ссылки на "локальные" объекты

public Thing calculateThing() {
    Thing thing = new Thing();
    // do calculations and modify thing
    return thing;
}

В C ++, чтобы сделать что-то подобное, у меня есть 2 варианта

(1) Я могу использовать ссылки всякий раз, когда мне нужно «вернуть» объект

void calculateThing(Thing& thing) {
    // do calculations and modify thing
}

Тогда используйте это как это

Thing thing;
calculateThing(thing);

(2) Или я могу вернуть указатель на динамически размещенный объект

Thing* calculateThing() {
    Thing* thing(new Thing());
    // do calculations and modify thing
    return thing;
}

Тогда используйте это как это

Thing* thing = calculateThing();
delete thing;

При первом подходе мне не придется освобождать память вручную, но для меня это затрудняет чтение кода. Проблема со вторым подходом заключается в том, что мне придется помнить delete thing;, что выглядит не очень хорошо. Я не хочу возвращать скопированное значение, потому что оно неэффективно (я думаю), поэтому здесь возникают вопросы

  • Есть ли третье решение (которое не требует копирования значения)?
  • Есть ли проблема, если я придерживаюсь первого решения?
  • Когда и почему я должен использовать второе решение?
phunehehe
источник
32
+1 за красиво поставленный вопрос.
Канкан
1
Чтобы быть очень педантичным, немного неточно говорить, что «функции возвращают что-то». Вернее, оценка вызова функции дает значение . Значение всегда является объектом (если это не void-функция). Различие заключается в том, является ли значение glvalue или prvalue - что определяется тем, является ли объявленный тип возврата ссылкой или нет.
Керрек SB

Ответы:

107

Я не хочу возвращать скопированное значение, потому что оно неэффективно

Докажите это.

Посмотрите RVO и NRVO, и в C ++ 0x семантика перемещения. В большинстве случаев в C ++ 03 параметр out - это всего лишь хороший способ сделать ваш код некрасивым, а в C ++ 0x вы действительно навредите себе, используя параметр out.

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


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

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

GManNickG
источник
10
@phunehehe: Нет смысла спекулировать, вы должны профилировать свой код и выяснить. (Подсказка: нет.) Компиляторы очень умны, они не будут тратить время на копирование, если не будут необходимости. Даже если копирование чего-то стоит, вы все равно должны стремиться к хорошему коду, а не к быстрому коду; хороший код легко оптимизировать, когда скорость становится проблемой. Нет смысла портить код для того, о чем вы даже не подозреваете; это проблема; особенно если вы на самом деле замедлите его или ничего не получите от него. И если вы используете C ++ 0x, семантика перемещения делает это проблемой.
GManNickG
1
@GMan, re: RVO: на самом деле это верно только в том случае, если ваши вызывающая сторона и вызываемая сторона оказываются в одной и той же единице компиляции, что в реальном мире не всегда. Таким образом, вас ждет разочарование, если ваш код не весь шаблонизирован (в этом случае он будет все в одном модуле компиляции) или у вас есть некоторая оптимизация во время компоновки (GCC имеет только 4.5).
Алекс Б
2
@Alex: компиляторы становятся все лучше и лучше при оптимизации по всем единицам перевода. (VC делает это для нескольких выпусков сейчас.)
sbi
9
@ Алекс Б: Это полная фигня. Многие очень распространенные соглашения о вызовах делают вызывающего абонента ответственным за выделение места для больших возвращаемых значений, а вызываемого - за их построение. RVO успешно работает во всех единицах компиляции даже без оптимизации времени компоновки.
CB Bailey
6
@ Чарльз, после проверки это кажется правильным! Я снимаю свое явно дезинформированное заявление.
Алекс Б
41

Просто создайте объект и верните его

Thing calculateThing() {
    Thing thing;
    // do calculations and modify thing
     return thing;
}

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

Амир Рахум
источник
2
Thing thing();объявляет локальную функцию и возвращает Thing.
Dreamlax
2
Thing thing () объявляет функцию, возвращающую вещь. В вашем теле функции не создан объект Thing.
CB Bailey
@dreamlax @Charles @GMan Немного поздно, но исправлено.
Амир Рахум
Как это работает в C ++ 98? Я получаю ошибки в интерпретаторе CINT, и мне было интересно, что это из-за C ++ 98 или самого CINT ...!
xcorat
16

Просто верните объект так:

Thing calculateThing() 
{
   Thing thing();
   // do calculations and modify thing
   return thing;
}

Это вызовет конструктор копирования в Things, так что вы можете захотеть сделать это самостоятельно. Как это:

Thing(const Thing& aThing) {}

Это может работать немного медленнее, но это не может быть проблемой вообще.

Обновить

Компилятор, вероятно, оптимизирует вызов конструктора копирования, поэтому никаких дополнительных затрат не будет. (Как и DreamLax указано в комментарии).

Мартин Ингвар Кофоед Дженсен
источник
9
Thing thing();объявляет локальную функцию, возвращающую Thing, также, стандарт позволяет компилятору опускать конструктор копирования в представленном вами случае; любой современный компилятор, вероятно, сделает это.
Dreamlax
1
Вы привнесли хорошую мысль для реализации конструктора копирования, особенно если требуется глубокая копия.
mbadawi23
+1 за явное указание на конструктор копирования, хотя, как говорит @dreamlax, компилятор, скорее всего, «оптимизирует» возвращаемый код для функций, избегая ненужного вызова конструктора копирования.
jose.angel.jimenez
В 2018 году, в VS 2017, он пытается использовать конструктор перемещения. Если конструктор перемещения удален, а конструктор копирования - нет, он не скомпилируется.
Андрей
11

Вы пытались использовать умные указатели (если Thing действительно большой и тяжелый объект), например, auto_ptr:


std::auto_ptr<Thing> calculateThing()
{
  std::auto_ptr<Thing> thing(new Thing);
  // .. some calculations
  return thing;
}


// ...
{
  std::auto_ptr<Thing> thing = calculateThing();
  // working with thing

  // auto_ptr frees thing 
}
Димитрия
источник
4
auto_ptrs устарели; использовать shared_ptrили unique_ptrвместо
MBraedley
Просто добавлю это сюда ... Я использую c ++ в течение многих лет, хотя и не профессионально с c ++ ... Я решил больше не использовать умные указатели, они просто бесполезны и вызывают все виды проблем, не очень помогающие ускорить код тоже. Я бы предпочел просто копировать данные и управлять указателями самостоятельно, используя RAII. Поэтому я советую избегать умных указателей.
Андрей
8

Один быстрый способ определить, вызывается ли конструктор копирования, - добавить запись в конструктор копирования вашего класса:

MyClass::MyClass(const MyClass &other)
{
    std::cout << "Copy constructor was called" << std::endl;
}

MyClass someFunction()
{
    MyClass dummy;
    return dummy;
}

Звонок someFunction; количество строк «Копировать конструктор был вызван», которые вы получите, будет варьироваться между 0, 1 и 2. Если вы не получили ни одной, ваш компилятор оптимизировал возвращаемое значение (что ему разрешено делать). Если вы не получаете 0, и ваш конструктор копирования смехотворно дорого, то искать альтернативные пути , чтобы вернуть экземпляры из ваших функций.

dreamlax
источник
1

Во-первых, у вас есть ошибка в коде, вы имеете в виду Thing *thing(new Thing());, и только return thing;.

  • Использование shared_ptr<Thing>. Разыщите его, как указатель. Он будет удален для вас при последнем обращении кThing содержимое выйдет из области видимости.
  • Первое решение очень распространено в наивных библиотеках. Он имеет некоторую производительность и синтаксические издержки, по возможности избегайте его
  • Используйте второе решение только в том случае, если вы можете гарантировать отсутствие исключений или если производительность абсолютно критична (вы будете взаимодействовать с C или сборкой до того, как это станет актуальным).
Мэтт Джойнер
источник
0

Я уверен, что эксперт C ++ придет с лучшим ответом, но лично мне нравится второй подход. Использование умных указателей помогает решить проблему забвения deleteи, как вы говорите, выглядит чище, чем необходимость создания объекта перед рукой (и все же необходимости его удаления, если вы хотите разместить его в куче).

EMP
источник