Когда не стоит переходить по ссылке? - PullRequest
5 голосов
/ 26 февраля 2009

Это проблема выделения памяти, которую я никогда не понимал.

void unleashMonkeyFish()  
{  
    MonkeyFish * monkey_fish = new MonkeyFish();
    std::string localname = "Wanda";  
    monkey_fish->setName(localname);  
    monkey_fish->go();  
}  

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

Теперь, когда я определяю элемент данных «name» внутри класса MonkeyFish, я могу выбрать один из следующих вариантов:

std::string name;
std::string & name;

Когда я определяю прототип для функции setName () внутри класса MonkeyFish, я могу выбрать один из следующих вариантов:

void setName( const std::string & parameter_name );
void setName( const std::string parameter_name );

Я хочу иметь возможность минимизировать количество копий строк. На самом деле, я хочу полностью устранить их, если смогу. Итак, похоже, что я должен передать параметр по ссылке ... правильно?

Что меня беспокоит, так это то, что моя локальная переменная выйдет из области действия после завершения функции unleashMonkeyFish (). Означает ли это, что я вынужден передать параметр при копировании? Или я могу передать его по ссылке и как-нибудь "сойти с рук"?

В принципе, я хочу избежать этих сценариев:

  1. Я не хочу устанавливать имя MonkeyFish только для того, чтобы память для строки localname исчезла, когда завершится функция unleashMonkeyFish (). (Кажется, это было бы очень плохо.)
  2. Я не хочу копировать строку, если смогу помочь.
  3. Я бы предпочел не использовать новое локальное имя

Какую комбинацию прототипа и члена данных я должен использовать?

РАЗЪЯСНЕНИЕ : В нескольких ответах предлагалось использовать ключевое слово static, чтобы гарантировать, что память автоматически не выделяется при завершении unleashMonkeyFish (). Поскольку конечной целью этого приложения является использование N MonkeyFish (все из которых должны иметь уникальные имена), этот вариант не подходит. (И да, MonkeyFish - непостоянные существа - часто меняют свои имена, иногда несколько раз в течение одного дня.)

EDIT : Грег Хьюгил указал, что хранить переменную name в качестве ссылки недопустимо, поскольку она не устанавливается в конструкторе. Я оставляю ошибку в вопросе как есть, так как думаю, что моя ошибка (и исправление Грега) может быть полезна для того, кто впервые видит эту проблему.

Ответы [ 9 ]

6 голосов
/ 26 февраля 2009

Один из способов сделать это - получить строку

std::string name;

Как элемент данных вашего объекта. А затем, в функции unleashMonkeyFish создайте строку, как вы это сделали, и передайте ее по ссылке , как вы показали

void setName( const std::string & parameter_name ) {
    name = parameter_name;
}

Он будет делать то, что вы хотите - создать одну копию, чтобы скопировать строку в ваш элемент данных. Это не значит, что он должен перераспределять новый буфер внутри, если вы назначаете другую строку. Возможно, назначение новой строки просто копирует несколько байтов. std :: string имеет возможность резервировать байты. Таким образом, вы можете вызвать «name.reserve (25);» в вашем конструкторе, и он, скорее всего, не будет перераспределен, если вы назначите что-то меньшее. (Я провел тесты, и похоже, что GCC всегда перераспределяет, если вы присваиваете из другой std :: string, но не если вы присваиваете из c-строки. Они говорят, у них есть копия при записи строка, которая объясняет это поведение).

Строка, которую вы создаете в функции unleashMonkeyFish, автоматически высвобождает выделенные ей ресурсы. Это ключевая особенность этих объектов - они сами управляют своими вещами. Классы имеют деструктор, который они используют для освобождения выделенных ресурсов после того, как объекты умирают, также есть и std :: string. На мой взгляд, вам не стоит беспокоиться о том, чтобы этот std :: string был локальным в функции. Это, скорее всего, не сделает ничего заметного для вашей работы. Некоторые реализации std :: string (msvc ++ afaik) имеют оптимизацию с небольшим буфером: до некоторого небольшого предела они сохраняют символы во встроенном буфере вместо выделения из кучи.

Редактировать :

Как оказалось, есть лучший способ сделать это для классов, которые имеют эффективную swap реализацию (постоянное время):

void setName(std::string parameter_name) {
    name.swap(parameter_name);
}

Причина, по которой это лучше, заключается в том, что теперь вызывающая сторона знает, что аргумент копируется. Оптимизация возвращаемого значения и аналогичные оптимизации теперь могут легко применяться компилятором. Рассмотрим этот случай, например

obj.setName("Mr. " + things.getName());

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

Для дальнейшего объяснения прочитайте отличную статью BoostCon09/Rvalue-References

5 голосов
/ 26 февраля 2009

Если вы используете следующее объявление метода:

void setName( const std::string & parameter_name );

тогда вы бы также использовали объявление члена:

std::string name;

и присвоение в теле setName:

name = parameter_name;

Вы не можете объявить элемент name как ссылку, потому что вы должны инициализировать элемент ссылки в конструкторе объекта (что означает, что вы не можете установить его в setName).

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

3 голосов
/ 26 февраля 2009

Просто для пояснения терминологии вы создали MonkeyFish из кучи (используя new) и локального имени в стеке.

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

Можете ли вы уточнить, почему вы не хотите копировать строку?

Редактировать

Альтернативный подход - создать пул объектов MonkeyName. Каждое MonkeyName хранит указатель на строку. Затем получите новое MonkeyName, запросив его из пула (задает имя во внутренней строке *). Теперь передайте это в класс по ссылке и выполните прямой обмен указателями. Конечно, переданный объект MonkayName изменяется, но если он возвращается прямо в пул, это не будет иметь значения. Единственное, что накладывает накладные расходы, - это фактическая настройка имени, когда вы получаете MonkeyName из пула.

... надеюсь, что это имело какой-то смысл:)

2 голосов
/ 23 июня 2009

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

class MonkeyFish {
public:
  void setName( const std::string & parameter_name ) { name = parameter_name; }

private:
  std::string name;
};

Это передаст ссылку на локальную строку, которая копируется в постоянную строку внутри объекта. Любые решения, которые включают нулевое копирование, чрезвычайно хрупки, потому что вам нужно быть осторожным, чтобы передаваемая строка оставалась в живых до тех пор, пока объект не будет удален. Лучше не ходите туда, если это не является абсолютно необходимым, и строковые копии не ТАК дороги - беспокойтесь об этом только тогда, когда это необходимо. : -)

2 голосов
/ 26 февраля 2009

В качестве простого практического правила храните ваши данные в виде копии внутри класса, а также передавайте и возвращайте данные по (const) ссылкам, по возможности используйте указатели подсчета ссылок.

Меня не очень беспокоит копирование нескольких тысяч байтов строковых данных до тех пор, пока профилировщик не скажет, что это значительная стоимость. OTOH Меня волнует, что структуры данных, которые содержат несколько десятков МБ данных, не копируются.

2 голосов
/ 26 февраля 2009

Когда компилятор видит ...

std::string localname = "Wanda";  

... он (исключая магию оптимизации) испустит 0x57 0x61 0x6E 0x64 0x61 0x00 [Ванда с нулевым терминатором] и сохранит его где-то в статическом разделе вашего кода. Затем он вызовет std :: string (const char *) и передаст ему этот адрес. Поскольку у автора конструктора нет возможности узнать время жизни предоставленного const char *, он должен сделать копию. В MonkeyFish :: setName (const std :: string &) компилятор увидит std :: string :: operator = (const std :: string &), и, если ваша std :: string реализована с помощью copy-on- При написании семантики компилятор будет генерировать код для увеличения количества ссылок, но не будет копировать.

Таким образом, вы заплатите за одну копию. Вам нужен хотя бы один? Знаете ли вы во время компиляции, как будут называться MonkeyFish? Меняют ли когда-нибудь MonkeyFish их имена на то, что не известно во время компиляции? Если все возможные имена MonkeyFish известны во время компиляции, вы можете избежать всего копирования, используя статическую таблицу строковых литералов и реализуя элемент данных MonkeyFish как const char *.

2 голосов
/ 26 февраля 2009

Это именно та проблема, которую должен решить подсчет ссылок. Вы можете использовать Boost shared_ptr <> для ссылки на строковый объект таким образом, чтобы он жил как минимум столько же, сколько каждый указатель на него.

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

1 голос
/ 26 февраля 2009

Если вы используете временную переменную для присвоения имени (как в вашем примере кода), вам в конечном итоге придется скопировать строку в ваш объект MonkeyFish, чтобы избежать попадания в конец области действия временного объекта строки. 1001 *

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

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

1 голос
/ 26 февраля 2009

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

Я перешел "вниз" из языков более высокого уровня (таких как C #, Java) и недавно столкнулся с этой же проблемой. Я предполагаю, что часто единственным вариантом является копирование строки.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...