Як "повернути об'єкт" в 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;, що виглядає не дуже добре. Я не хочу повертати скопійоване значення, оскільки воно неефективне (я думаю), тому тут виникають питання

  • Чи є третє рішення (для якого не потрібно копіювати значення)?
  • Чи є якась проблема, якщо я дотримуюся першого рішення?
  • Коли і чому я повинен використовувати друге рішення?

32
+1 за гарне ставлення питання.
Кангкан

1
Щоб бути дуже педантичним, трохи неточно сказати, що "функції щось повертають". Більш правильно, оцінюючи виклик функції, створює значення . Значення - це завжди об'єкт (якщо тільки це недійсна функція). Відмінність полягає в тому, чи є значення glvalue або первісне значення - яке визначається тим, чи оголошений тип повернення є еталонним чи ні.
Керрек СБ

Відповіді:


107

Я не хочу повертати скопійоване значення, оскільки це неефективно

Докажи це.

Знайдіть RVO та NRVO, а також у C ++ 0x семантику руху. У більшості випадків у C ++ 03 параметр «out» - це лише хороший спосіб зробити ваш код некрасивим, а в C ++ 0x ви насправді нашкодите собі, використовуючи параметр «out».

Просто напишіть чистий код, поверніть значення. Якщо продуктивність є проблемою, профіліруйте її (перестаньте здогадуватися) та знайдіть, що можна зробити, щоб її виправити. Це, ймовірно, не поверне речі з функцій.


Однак це означає, що якщо ви померли так, щоб писати так, ви, мабуть, захочете виконати параметр out. Це дозволяє уникнути динамічного розподілу пам’яті, що безпечніше і, як правило, швидше. Це вимагає у вас певного способу побудови об'єкта до виклику функції, що не завжди має сенс для всіх об'єктів.

Якщо ви хочете використовувати динамічний розподіл, найменше, що можна зробити, - це помістити його в інтелектуальний покажчик. (Це потрібно робити весь час у будь-якому випадку) Тоді ви не турбуєтесь про те, щоб видалити що-небудь, речі безпечні для винятків тощо. Єдина проблема - це, швидше за все, повільніше, ніж повернення за значенням у будь-якому разі!


10
@phunehehe: Немає сенсу міркувати, ви повинні профайлювати код і дізнатися це. (Підказка: ні.) Компілятори дуже розумні, вони не збираються витрачати час на копіювання речей, якщо цього не потрібно. Навіть якщо копіювання чогось коштує, вам все одно слід прагнути до хорошого коду над швидким кодом; хороший код легко оптимізувати, коли швидкість стає проблемою. Немає сенсу принижувати код для чогось, про що ти поняття не маєш, це проблема; особливо якщо ви насправді сповільнюєте це або нічого не отримуєте з нього. І якщо ви використовуєте C ++ 0x, семантика переміщення робить це не проблемою.
GManNickG

1
@GMan, re: RVO: насправді це справедливо лише в тому випадку, якщо ваш абонент і абонент перебувають у тому самому блоці компіляції, що в реальному світі це не більшість часу. Отже, вас чекає розчарування, якщо ваш код не є всім шаблоном (у такому випадку він буде знаходитися в одній компіляційній одиниці) або у вас є певна оптимізація часу зв’язку (GCC має його лише з 4.5).
Алекс Б

2
@Alex: Компілятори стають все кращими та кращими в оптимізації для перекладацьких одиниць. (VC робить це вже для кількох релізів.)
sbi

9
@ Алекс В: Це повне сміття. Багато дуже поширених угод про виклики змушують абонента відповідальний за розподіл місця для великих повернених значень, а абонента - відповідального за їх побудову. RVO із задоволенням працює у складі одиниць компіляції навіть без оптимізації часу зв'язку.
CB Bailey

6
@Charles, після перевірки виявляється, що це правильно! Я відкликаю свою явно неправильну заяву.
Алекс Б

41

Просто створіть об’єкт і поверніть його

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

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


2
Thing thing();оголошує локальну функцію та повертає a 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;
}

Це призведе до виклику конструктора копій на "Речі", тому ви, можливо, захочете виконати власну реалізацію цього. Подобається це:

Thing(const Thing& aThing) {}

Це може працювати трохи повільніше, але це може бути зовсім не проблемою.

Оновлення

Компілятор, ймовірно, оптимізує виклик до конструктора копій, тому зайвих накладних витрат не буде. (Ніби dreamlax вказував у коментарі).


9
Thing thing();оголошує локальну функцію, що повертає a Thing, також стандарт дозволяє компілятору опускати конструктор копій у випадку, який ви представили; будь-який сучасний компілятор, ймовірно, зробить це.
dreamlax

1
Ви добре підходите до впровадження конструктора копій, особливо якщо потрібна глибока копія.
mbadawi23

+1 для явного твердження про конструктор копіювання, хоча, як каже @dreamlax, компілятор, швидше за все, "оптимізує" код, що повертається, для функцій, уникаючи не дуже потрібного виклику конструктора копіювання.
jose.angel.jimenez

У 2018 році у VS 2017 він намагається використовувати конструктор ходу. Якщо конструктор переміщення буде видалено, а конструктор копії - ні, він не буде компілюватися.
Андрій

11

Чи намагалися ви використовувати розумні покажчики (якщо річ дійсно великий і важкий об’єкт), наприклад, 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 ++ ... Я вирішив більше не намагатися використовувати смарт-покажчики, вони просто абсолютний безлад imo і викликають все всілякі проблеми, не дуже допомагаючи також прискорити код. Я так багато скоріше просто копіювати дані навколо та керувати вказівниками самостійно, використовуючи 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, і ваш конструктор копіювання сміховинно дорого, то шукати альтернативні шляхи , щоб повернути екземпляри з ваших функцій.


1

По-перше, у вас є помилка в коді, ви маєте на увазі наявність Thing *thing(new Thing());і тільки return thing;.

  • Використовуйте shared_ptr<Thing>. Deref це як вказівник. Він буде видалений для вас, коли остання посилання на Thingміститься не виходить за межі.
  • Перше рішення дуже поширене в наївних бібліотеках. Він має деяку продуктивність і синтаксичні накладні витрати, уникайте цього, якщо можливо
  • Використовуйте друге рішення лише в тому випадку, якщо ви можете гарантувати, що винятки не будуть викинуті, або коли продуктивність абсолютно критична (ви будете взаємодіяти з C або збіркою, перш ніж це навіть стане актуальним).

0

Я впевнений, що експерт C ++ прийде разом з кращою відповіддю, але особисто мені подобається другий підхід. Використання розумних покажчиків допомагає вирішити проблему забуття, deleteі, як ви кажете, це виглядає більш чистою, ніж створювати об'єкт перед рукою (і все-таки потрібно видалити його, якщо ви хочете виділити його на купу).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.