При возврате объекта зачем помещать создание + инициализацию и возврат в виде двух отдельных операторов вместо одного?

Пример:

Foo make_foo(int a1, int a2){
  Foo f(a1,a2);
  return f;
}

Видел такие функции несколько раз, это просто вопрос стиля/предпочтения кодирования или это нечто большее, чем кажется на первый взгляд? В частности, этот ответ заставил меня задуматься над реализацией make_unique и ее утверждением безопасно ли исключение - связано ли это с разделением создания и возврата? Или я слишком много читаю об этом? Почему бы просто не написать

Foo make_foo(int a1, int a2){
  return Foo(a1,a2);
}

person Xeo    schedule 01.06.2011    source источник


Ответы (5)


Обратите внимание, что ответ, на который вы ссылаетесь, на самом деле имеет нечто иное:

std::unique_ptr<T> ret (new T(std::forward<Args>(args)...));

В этой строке кода выполняется явное динамическое выделение. Лучшие практики требуют, чтобы всякий раз, когда вы выполняете явное динамическое выделение, вы должны немедленно назначать результат именованному интеллектуальному указателю. Дополнительные сведения см. в документации по лучшим практикам Boost shared_ptr или статью Herb Sutter в GotW, "Вызовы функций, безопасные для исключений."

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


Тем не менее, есть по крайней мере одно преимущество шаблона создания именованного объекта и последующего его возврата: может быть немного проще "наблюдать" за объектом в отладчике при быстром выполнении кода.

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

person James McNellis    schedule 01.06.2011
comment
Но зачем умному указателю named? Почему не unique_ptr<T>(new T(...))? - person Xeo; 01.06.2011
comment
@James, но не возвращает std::unique_ptr‹T› ret(new T(std::forward‹args›(args)...)); Работа? - person johnathan; 01.06.2011
comment
@johnathon: Да, но идея в том, что если вы последовательно будете следовать шаблону назначения права собственности на динамически выделяемые объекты именованным умным указателям, у вас гораздо меньше шансов случайно совершить ошибку и в итоге получить не безопасный для исключений код. Шаблоны хороши, особенно когда речь идет о коде, в котором часто возникают ошибки, как в случае с кодом, который имеет дело с временем жизни объекта и владением. - person James McNellis; 01.06.2011
comment
@Xeo, @johnathon: Проблема с безопасностью исключений, упомянутая Джеймсом, заключается в том, что если бы у меня было что-то вроде: fooFunc(new A(), g()), то если выполняется первый new A() и выбрасывается g(), у меня есть утечка. Так как такую ​​деталь легко упустить (особенно при добавлении аргумента или замене функции, которая не выдает исключение, на функцию, которая выдает исключение), шаблон, о котором говорит Джеймс, является строгим для предотвращения ошибок. - person Matthieu M.; 01.06.2011
comment
Или, что более пагубно, рассмотрите случай с аргументами по умолчанию, например. void f(A*, B* = g()); вызывается как f(new A()), где g() бросает. На месте вызова он выглядит нормально, но на самом деле он довольно сломан. @Матье - person James McNellis; 01.06.2011
comment
Проблема заключается в том, чтобы избежать создания более одного выделения/умного указателя в одном выражении. Это все. Это не причина не помещать заявление в отчет. - person James Kanze; 01.06.2011
comment
@James: Это не совсем точно: в примере из моего предыдущего комментария есть только одно распределение. Да, вы правы: во многих случаях совершенно безопасно использовать new в контексте, отличном от инициализации или назначения именованного интеллектуального указателя. Однако трудно быстро определить, где безопасно, а где нет, и легко забыть, когда вам абсолютно необходим именованный интеллектуальный указатель, поэтому лучший способ действий — следовать рекомендациям и всегда использовать именованный интеллектуальный указатель. указатель. Стоимость низкая (немного больше печатать) польза большая (постоянство и корректность). - person James McNellis; 01.06.2011
comment
@James McNellis Пока для создания класса RAII для каждого оператора выделяется и используется только один ресурс, проблем нет. Я не понимаю, как это трудно проанализировать или идентифицировать. Возврат интеллектуального указателя с немедленно newed объектом является более или менее стандартной идиомой. - person James Kanze; 01.06.2011

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

person johnathan    schedule 01.06.2011

Это полностью зависит от ваших требований, и вы можете поместить их в один оператор. Это сделано для того, чтобы сделать код более читаемым.

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

person Algorithmist    schedule 01.06.2011

Лично каждый раз, когда я пишу шаблон класса, я также пишу соответствующий make_ шаблон функции, позволяющий создавать объекты типа шаблона класса без явного указания аргументов шаблона. Конечно, обычно это происходит только при использовании компилятора с семантикой C++0x auto или при передаче временного параметра в качестве аргумента функции, но, на мой взгляд, оба эти сценария достаточно распространены, чтобы оправдать усилия.

Тем не менее, я никогда не писал подобный код для создания нешаблонного типа.

person ildjarn    schedule 01.06.2011

Исторически первая версия

Foo make_foo(int a1, int a2)
{
    Foo f(a1,a2);
    return f; 
} 

имел больше шансов запустить оптимизацию NRVO в некоторых компиляторах (например, в старых MSVC).

Версия 2 должна работать так же хорошо для компиляторов, реализующих RVO, но исторически это было не так надежно.

person Bo Persson    schedule 01.06.2011
comment
У меня сложилось впечатление, что RVO безымянного временного объекта гораздо более вероятен, чем NRVO: компилятор знает, что объект создается здесь и сейчас, и он знает, что если он будет скопирован туда, где должен быть результат, поэтому он всегда можно исключить эту копию (игнорируя исключения, которые могут помешать любому виду RVO). Почему вы говорите, что NRVO более вероятен, чем безымянный RVO? - person James McNellis; 01.06.2011
comment
@James - я говорю, что раньше это было в более ранних версиях MSVC. Создание именованного объекта и его возврат вызвало больше оптимизаций, чем возврат временного объекта. Очевидно, это затронуло некоторые кодовые базы и рекомендации. - person Bo Persson; 01.06.2011