Возврат константной ссылки на объект вместо копии

Во время рефакторинга кода я наткнулся на несколько методов получения, которые возвращают std :: string. Примерно так:

class foo
{
private:
    std::string name_;
public:
    std::string name()
    {
        return name_;
    }
};

Неужто геттеру было бы лучше вернуть const std::string&? Текущий метод возвращает копию, которая не так эффективна. Вместо этого вызовет ли возврат константной ссылки какие-либо проблемы?

Кто сказал, что это неэффективно. Над std :: string было проделано много работы, чтобы сделать эту операцию эффективной. Есть разница между передачей ссылки и strincg, но реальная разница обычно несущественна.

Martin York 25.09.2008 22:03

@Loki: ничто не говорит о том, что это будет эффективно, и на самом деле самая последняя версия C++ говорит, что, вероятно, будет. единственной оптимизацией, которая могла бы сделать этот код достаточно эффективным, была бы std :: string с подсчетом ссылок. Однако многие реализации STL не выполняют подсчет ссылок, так как на современных многоядерных процессорах управление счетчиком ссылок происходит медленнее, чем вы думаете. Так что да, возврат копии происходит намного медленнее. Ни GCC, ни Microsoft STL уже много лет не проводят подсчет ссылок на std :: string.

Sean Middleditch 26.10.2011 21:19

@seanmiddleditch: Всегда измеряйте, прежде чем предполагать (это все, что я говорю). Есть также много других оптимизаций, которые может применить компилятор: встраивание и копирование будут двумя, что снизит стоимость до нуля.

Martin York 26.10.2011 22:06

@Loki: да, это хороший совет; всегда профиль. Также хороший совет - знать разницу между «предположением» и «фактом». Я гарантирую вам, что код Роба никогда не приведет к нулевым накладным расходам в любой реализации соответствующего компилятора C++. Язык требует, чтобы конструктор копирования был вызван хотя бы один раз при возврате из foo :: name (), даже при встраивании (не влияет на вызов конструкторов), copy elision (не применимо к этому коду) и RVO ( может уменьшить количество дополнительных вызовов конструктора копирования, но не устранить их все).

Sean Middleditch 31.10.2011 11:40

@seanmiddleditch: Да, вы используете в качестве аргумента раздел 12.8 пар. 32 (укажите этот раздел при цитировании стандарта в аргументе). Приношу свои извинения за использование термина «копирование», поскольку он гораздо более конкретен, чем я ожидал. Но я не согласен с тем, что у него нулевые накладные расходы. Вы забываете, что правило As-If (1.9, пункт 1) также может быть использовано здесь для удаления любого избыточного кода, который не имеет побочных эффектов. Пожалуйста, попробуйте измерить это.

Martin York 31.10.2011 22:56

@Loki: Я сделал пытаюсь измерить его как на GCC 4.6, так и на VC 10, оба с максимальной оптимизацией, и получил те же результаты: вызывается конструктор копирования. Это «очевидный» результат, даже если принять во внимание правило as-if, поскольку конструктор копирования std :: string на самом деле имеет побочные эффекты: он либо создает полностью новый массив символов и копию строки, либо манипулирует значение счетчика ссылок (в зависимости от реализации). Заметьте, что достаточно простой микротест на имплантате с подсчетом ссылок может не дать вам «реальных» результатов, с которыми я столкнулся на GCC с первой попытки.

Sean Middleditch 04.11.2011 22:36
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
73
6
97 933
12
Перейти к ответу Данный вопрос помечен как решенный

Ответы 12

Я бы изменил его, чтобы он возвращал const std :: string &. Вызывающий, вероятно, все равно сделает копию результата, если вы не измените весь вызывающий код, но это не вызовет никаких проблем.

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

Взгляните на ответ Димы на связанную потенциальную, но маловероятную проблему.

Зависит от того, что вам нужно делать. Возможно, вы хотите, чтобы все вызывающие изменили возвращаемое значение без изменения класса. Если вы вернете ссылку на const, которая не сработает.

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

Не думаю, что это проблема. Вызывающий может просто выполнить «std :: string s = aFoo.name ()», и s будет изменяемой копией.

Kristopher Johnson 25.09.2008 21:37

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

Самый простой способ - это попробовать, а затем проверить, работает ли он по-прежнему, при условии, что у вас есть какой-то тест, который вы можете запустить. В противном случае я бы сосредоточился на написании теста, прежде чем продолжить рефакторинг.

Ответ принят как подходящий

Единственный способ, которым это может вызвать проблему, - это если вызывающий объект сохраняет ссылку, а не копирует строку, и пытается использовать ее после уничтожения объекта. Нравится:

foo *pFoo = new foo;
const std::string &myName = pFoo->getName();
delete pFoo;
cout << myName;  // error! dangling reference

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

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

Не нарушит существующий код, но не получит преимущества в производительности Любые. Если бы проблемы было так же легко обнаружить, как в вашем примере, это было бы хорошо, но часто это может быть намного сложнее ... например, если у вас есть vector <foo> и push_back, вызывая изменение размера и т. д. Преждевременное оптимизация и т. д. и т. д.

tfinniga 25.09.2008 22:36

Конечно, вы получаете преимущество в производительности. Если вы возвращаете объект, вы оплачиваете стоимость создания дополнительной копии объекта, временной, которая возвращается. Вы не платите эту стоимость, если вернете ссылку.

Dima 25.09.2008 23:06

@Dima: вполне вероятно, что в этом случае большинство компиляторов выполнят оптимизацию возвращаемого значения именованного значения.

Richard Corden 26.09.2008 13:35

@ Ричард: да, я недавно читал об этом. Но это касается только случая, когда вы сразу же присваиваете возвращаемое значение чему-то, не так ли? Что делать, если возвращаемое значение передается функции? Будет ли еще оптимизировано строительство временного?

Dima 30.09.2008 23:02

Я думаю, вы правы в том, что это применимо только в том случае, если вы упомянули

BlueTrin 19.04.2011 18:53

Так каков был окончательный вердикт? Лучше передать ссылку обратно или просто передать копию? Предположим, компилятор тупой, а программист должен быть конкретным и вдумчивым, как в те времена, когда мы не ленились :)

Volte 09.07.2012 16:51

@Dale, я бы посоветовал вернуть константную ссылку.

Dima 09.07.2012 19:10

Пункт насчет РВО вводит в заблуждение. RVO избавит вас от ненужных копий / ходов, но не избавит от создания копии члена. Если ваша строка члена - это строка размером 1 КБ, вы все равно выделите 1 КБ при возврате по значению.

NoSenseEtAl 24.08.2020 18:01

Скорее всего, типичное использование этой функции не сломается, если вы перейдете на константную ссылку.

Если весь код, вызывающий эту функцию, находится под вашим контролем, просто внесите изменения и посмотрите, жалуется ли компилятор.

Обычно я возвращаю const &, если не могу. QBziZ дает пример того, где это так. Конечно, QBziZ также утверждает, что std :: string имеет семантику копирования при записи, которая сегодня редко бывает верной, поскольку COW влечет за собой много накладных расходов в многопоточной среде. Возвращая const &, вы возлагаете на вызывающего абонента ответственность за правильные действия со строкой на его конце. Но поскольку вы имеете дело с кодом, который уже используется, вам, вероятно, не следует его изменять, если профилирование не показывает, что копирование этой строки вызывает серьезные проблемы с производительностью. Затем, если вы решите его поменять, вам нужно будет тщательно протестировать, чтобы убедиться, что вы ничего не сломали. Надеюсь, другие разработчики, с которыми вы работаете, не делают схематичных вещей, как в ответе Димы.

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

См. По этому поводу FAQ по C++ lite.

Оптимизация возврата по значению настолько сложна, что будет использовать указатель / ссылку на исходное значение члена i.o. копии? Я не думаю, что это так. Оптимизация Rbv пропускает несколько шагов при возврате значения, но по-прежнему вызывает ctor копирования cstring, хотя и версию на месте.

QBziZ 27.09.2008 00:00

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

M.M 12.11.2019 05:36

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

const std::string & str = myObject.getSomeString() ;

При возврате std::string временный объект останется живым и будет прикреплен к str до тех пор, пока str не выйдет за пределы области видимости.

Но что происходит с const std::string &? Я предполагаю, что у нас будет константная ссылка на объект, который может умереть, когда его родительский объект освободит его:

MyObject * myObject = new MyObject("My String") ;
const std::string & str = myObject->getSomeString() ;
delete myObject ;
// Use str... which references a destroyed object.

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

Некоторые реализации std :: string совместно используют память с семантикой копирования при записи, поэтому возврат по значению может быть почти таким же эффективным, как возврат по ссылке и, вам не нужно беспокоиться о проблемах времени жизни (среда выполнения делает это для вас).

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

Вы знаете, что они говорят о предположениях ...

Вместо этого вам нужно беспокоиться о безопасности потоков при изменении несвязанных строк :)

bk1e 26.09.2008 08:32

Хех. Что дает среда выполнения, многопоточность забирает. :)

rlerallut 26.09.2008 12:56

Из любопытства - реализует ли g ++ std :: string с семантикой копирования при записи?

Frank 31.03.2009 18:46

@ user60628 COW впала в немилость (десять лет назад) см. эта статья

Motti 06.01.2011 16:37

COW фактически запрещен в C++ 11 и более поздних версиях.

M.M 11.02.2015 05:36

Итак, различия между возвратом копии и возвратом ссылки:

  • Представление: возврат ссылки может быть быстрее, а может и нет; это зависит от того, как std::string реализован вашей реализацией компилятора (как указывали другие). Но даже если вы вернете ссылку, присваивание после вызова функции обычно включает копию, как в std::string name = obj.name();.

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

Если вы хотите быстро и безопасно, используйте boost :: shared_ptr. Ваш объект может внутренне хранить строку как shared_ptr и возвращать shared_ptr. Таким образом, не будет происходить копирование объекта, и это всегда безопасно (если только ваши пользователи не извлекут необработанный указатель с get() и не сделают с ним что-нибудь после того, как ваш объект выйдет из области видимости).

Фактически, другая проблема конкретно с возвратом строки нет по ссылке заключается в том, что std::string предоставляет доступ через указатель на внутренний const char* через метод c_str (). Это вызвало у меня многочасовую головную боль отладки. Например, предположим, что я хочу получить имя из foo и передать его JNI, чтобы использовать его для создания jstring для передачи в Java позже, и что name() возвращает копию, а не ссылку. Я мог бы написать что-то вроде этого:

foo myFoo = getFoo(); // Get the foo from somewhere.
const char* fooCName = foo.name().c_str(); // Woops!  foo.name() creates a temporary that's destructed as soon as this line executes!
jniEnv->NewStringUTF(fooCName);  // No good, fooCName was released when the temporary was deleted.

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

Блин, хороший улов. Хотя я думаю, что C++ 11 изменил срок службы временных файлов, чтобы сделать этот код пригодным для любого совместимого компилятора.

Mark McKenna 08.05.2017 16:22

@MarkMcKenna Это хорошо. Я немного поискал, и этот ответ, похоже, указывает на то, что вам все еще нужно быть немного осторожным, даже с C++ 11: stackoverflow.com/a/2507225/13140

Ogre Psalm33 15.05.2017 15:19

Возврат ссылки на член раскрывает реализацию класса. Это могло помешать изменить класс. Может быть полезно для частных или защищенных методов, если требуется оптимизация. Что должен возвращать геттер C++

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