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

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

В 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;, который выглядит не очень хорошо. Я не хочу возвращать скопированное значение, потому что оно неэффективно (я думаю), поэтому здесь возникают вопросы

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

7 ответов

Решение

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

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

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

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


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

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

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

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

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

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

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

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

Thing(const Thing& aThing) {}

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

Обновить

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

Вы пытались использовать умные указатели (если 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 
}

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

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

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

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

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

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

Например, GCC выполняет эту оптимизацию. В следующей программе не вызываются ни конструктор перемещения, ни конструктор копирования, поскольку копирование или перемещение не выполняется. Также обратите внимание на адрес c. Хотя объект c создается внутри функции f(), c находится в стековом фрейме main().

class C {
public:
    int c = 5;
    C() {}
    C(const C& c) { 
        cout << "Copy constructor " << endl;
    }
    C(const C&& c)  noexcept {
        cout << "Move Constructor" << endl;
    }
};

C f() {
    int beforeC;
    C c;
    int afterC;

    cout << &beforeC << endl;   //0x7ffee02f26ac
    cout << &c << endl;         //0x7ffee02f2710 (notice: even though c is instantiated inside f(), c resides in the stack frame of main()
    cout << &afterC << endl;    //0x7ffee02f26a8

    return c;
}

C g() {
    C c = f(); ///neither copy constructor nor move constructor of C are called, since none is done
    cout << &c << endl;  //0x7ffee02f2710
    return c;
}

int main() {
    int beforeC;
    C c = g();    ///neither copy constructor nor move constructor of C are called, since none is done
    int afterC;

    cout << &beforeC << endl; //0x7ffee02f2718 
    cout << &c << endl;       //0x7ffee02f2710 (notice:even though c is returned from f,it resides in the stack frame of main)
    cout << &afterC << endl;  //0x7ffee02f270c
    return 0;
}

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

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

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

Другие вопросы по тегам