Когда переходить по ссылке не рекомендуется?

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

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 );

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

Что меня беспокоит, так это то, что кажется, что моя переменная localname выходит за пределы области видимости после завершения функции unleashMonkeyFish (). Означает ли это, что я ПРИНУЖДЕН передать параметр копией? Или можно передать по ссылке и как-нибудь "сойти с рук"?

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

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

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

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

ИЗМЕНИТЬ: Грег Хьюджил указал, что хранить переменную имени в качестве ссылки незаконно, поскольку она не устанавливается в конструкторе. Я оставляю ошибку в вопросе как есть, так как думаю, что моя ошибка (и исправление Грега) может быть полезна тем, кто впервые видит эту проблему.


person Runcible    schedule 26.02.2009    source источник
comment
Вы профилировали и определили, что это проблема?   -  person CTT    schedule 26.02.2009
comment
@CTT - Нет, не видел. Однако копирование строки кажется неэффективным, если я могу этого избежать. В общем, я хочу по возможности избегать копирования.   -  person Runcible    schedule 26.02.2009
comment
monkey_fish находится в куче, а не в стеке.   -  person Ed S.    schedule 26.02.2009
comment
Если вы хотите, чтобы память, выделенная для localname, исчезла при выходе из функции, то по определению вам понадобится setName для копирования этих данных в какой-нибудь более долговечный объект.   -  person Crashworks    schedule 26.02.2009


Ответы (9)


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

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

person Johannes Schaub - litb    schedule 26.02.2009

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

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

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

std::string name;

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

name = parameter_name;

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

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

person Greg Hewgill    schedule 26.02.2009
comment
Да, хорошее замечание по поводу ссылки. Это моя ошибка. Он должен быть установлен только в конструкторе. - person Runcible; 26.02.2009

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

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

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

Изменить

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

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

person Gian Paolo    schedule 26.02.2009
comment
Что касается того, почему я не хочу копировать строку - это именно то, что вы сказали: я буду делать это много, много. В мире недостаточно MonkeyFish. - person Runcible; 26.02.2009

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

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

person Crashworks    schedule 26.02.2009
comment
Справедливо ли сказать, что решение litb вызывает копирование, но использование shared_ptrs позволит избежать копирования? - person Runcible; 26.02.2009
comment
shared_ptr обязывает вас создать новое локальное имя внутри unleashMonkeyFish (), поскольку оно должно находиться в куче, а не в стеке, если вы хотите, чтобы он пережил область действия функции. На практике они, вероятно, компилируются с одинаковым количеством копий, поскольку конструктор new std :: string () тоже должен делать копию! - person Crashworks; 26.02.2009
comment
shared_ptr нужно будет создать новую строку. Я утверждаю, что это хуже, чем просто скопировать 5 байтов символов, чем выделить 4 байта из кучи (а затем удалить старую строку, на которую раньше указывал ваш data-member-shared_ptr) :) - person Johannes Schaub - litb; 26.02.2009
comment
если вы получили std :: string непосредственно как член данных, тогда он может предварительно выделить буфер и не должен перераспределять всегда, если вы повторно установите его на какое-то значение. поэтому я не уверен, что решение shared_ptr поможет ему в чем-нибудь (я думаю, скорее, замедлит его). но в любом случае хороший момент. - person Johannes Schaub - litb; 26.02.2009
comment
Спасибо вам обоим за ваши комментарии. Это очень полезно! - person Runcible; 26.02.2009

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

std::string localname = "Wanda";  

... он (запретив магию оптимизации) выдаст 0x57 0x61 0x6E 0x64 0x61 0x00 [Wanda с нулевым терминатором] и сохранит его где-нибудь в статической части вашего кода. Затем он вызовет 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 *.

person Thomas L Holaday    schedule 26.02.2009
comment
Спасибо за отзыв tlholaday. Я добавил несколько поясняющих комментариев - но чтобы конкретно ответить на ваш вопрос: я не знаю, сколько будет MonkeyFish, и я не знаю их имен во время компиляции. - person Runcible; 26.02.2009

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

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

person lilburne    schedule 26.02.2009

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

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

private:
  std::string name;
};

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

person Frederik Slijkerman    schedule 23.06.2009

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

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

person Andrew Flanagan    schedule 26.02.2009

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

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

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

person e.James    schedule 26.02.2009