2009-02-26 2 views
5

Это проблема распределения памяти, которую я никогда не понимал.Когда это не очень хорошая идея?

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

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

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

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

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

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

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

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

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

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

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

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

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

+0

Профилированы ли вы и решили, что это проблема? – CTT

+0

@ CTT - Нет, нет. Однако, кажется, неэффективно копировать строку, если я могу ее избежать. В общем, я хочу избежать копирования как можно больше. – Runcible

+0

«monkey_fish» находится в куче, а не в стеке. –

ответ

6

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

std::string name; 

В дата-члена объекта.А потом, в unleashMonkeyFish функции создать строку, как вы делали, и передать его по ссылке как вы показали

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

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

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

Edit:

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

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

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

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

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

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

1

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

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

1

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

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

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

3

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

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

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

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

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

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

+0

Спасибо за исправление - обновление сообщения. – Runcible

+0

Насколько я не хочу копировать строку - это точно так же, как вы сказали: я собираюсь делать это много, много. В мире не хватает MonkeyFish. – Runcible

5

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

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

, то вы бы также использовать декларацию члена:

std::string name; 

и назначение в корпусе setName:

name = parameter_name; 

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

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

+0

Да, хороший момент относительно ссылки. Это моя ошибка. Он должен быть установлен только в конструкторе. – Runcible

2

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

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

+0

Справедливости ради стоит сказать, что решение litb вызывает копирование, но использование shared_ptrs позволит избежать копирования? – Runcible

+0

shared_ptr обязывает вас «обновлять» локальное имя внутри unleashMonkeyFish(), поскольку оно должно находиться в куче, а не в стеке, если вы хотите, чтобы он перестал работать с областью действия. На практике они, вероятно, скомпилируются с одинаковым количеством копий, так как новый конструктор std :: string() должен сделать копию тоже! – Crashworks

+0

shared_ptr потребуется новая строка. я утверждаю, что это хуже, чем просто копирование 5 байтов символов, чем выделение 4 байтов из кучи (а затем удаление старой строки, на которую указывал ваш data-member-shared_ptr) :) –

2

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

std::string localname = "Wanda"; 

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

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

+0

Спасибо за отзыв tlholaday. Я добавил некоторые пояснения, но, чтобы ответить на конкретный вопрос: я не знаю, сколько будет MonkeyFish, и я не знаю их имен во время компиляции. – Runcible

2

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

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

2

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

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

private: 
    std::string name; 
}; 

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

Смежные вопросы