2011-01-13 4 views
7

Я просто думал о реализации std::string::substr. Он возвращает новый объект std::string, который кажется мне немного расточительным. Почему бы не вернуть объект, который ссылается на содержимое исходной строки и может быть неявно назначен для std::string? Это своего рода ленивая оценка фактического копирования. Такой класс может выглядеть примерно так:Можно ли разрешить эту оптимизацию при реализации std :: string?

template <class Ch, class Tr, class A> 
class string_ref { 
public: 
    // not important yet, but *looks* like basic_string's for the most part 

private: 
    const basic_string<Ch, Tr, A> &s_; 
    const size_type pos_; 
    const size_type len_;  
}; 

Общедоступный интерфейс этого класса будет имитировать все операции только для чтения вещественного std::string, поэтому использование будет бесшовным. std::string может иметь новый конструктор, который принимает string_ref, чтобы пользователь никогда не был более мудрее. В тот момент, когда вы пытаетесь «сохранить» результат, вы в конечном итоге создаете копию, поэтому никаких реальных проблем с ссылкой, указывающей на данные, а затем ее изменения за ее спиной не возникает.

Идея заключается в том, что такой код:

std::string s1 = "hello world"; 
std::string s2 = "world"; 
if(s1.substr(6) == s2) { 
    std::cout << "match!" << std::endl; 
} 

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

Насколько я знаю, никакие реализации этого не делают.

Я предполагаю, что сердцевина вопроса:

Учитывая класс, который может быть неявно преобразован в std::string по мере необходимости, было бы, соответствующей стандарту для библиотеки писателя, чтобы изменить прототип члена в тип возврата? Или, в более общем плане, у библиотекарей есть возможность вернуть «прокси-объекты» вместо обычных объектов в этих типах случаев в качестве оптимизации?

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

+3

@Evan: вы можете прочитать язык программирования C++ - он представляет собой реализацию этой концепции. Тем не менее, такой string_ref легко недействителен, а копирование часто относительно дешево, поэтому обычно люди с удовольствием копируют в новую строку. Когда я реализую такой класс, я стараюсь сделать это в терминах const char * и size, что делает его более широко применимым (например, накладывая его на файлы с отображением памяти, данные const char [] и т. Д.). Если вам нужна подстрока, которая может быть изменена независимо от источника, тогда все быстро усложняется, особенно в поточных средах .... –

+0

@ Тони: как 'llvm :: StringRef'? –

+0

Вы не думаете, что реализация COW std :: string еще не повторно использует буфер исходной строки, когда вы выполняете substr(). –

ответ

6

Эта идея copy-on-write, но вместо COW'ing всего буфера вы отслеживаете, какой поднабор буфера является «реальной» строкой. (COW в своей нормальной форме использовался (есть?), Используемый в некоторых библиотечных реализациях.)

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

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

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

+1

Копирование на запись или субблок всей памяти? Интересно, действительно ли какая-либо библиотека поддерживала это (по сравнению с копированием на запись всей строки) –

+0

@David: это COW, но отслеживание того, какое подмножество памяти на самом деле копируется. И хороший вопрос, я не думаю, что когда-либо слышал об этом. Накладные расходы самой COW вообще не стоят в наши дни. – GManNickG

+0

@GMan: для того, чтобы отслеживать все интервалы (загляните в IntervalTree в Википедии), вам понадобится какой-то механизм, поэтому я думаю, что было бы проще (и на самом деле быстрее) на самом деле COW все это ...если std :: string не было разрешено иметь несмежный макет (например, класс STL 'rope), и в этом случае вы могли бы поддерживать подсчет по блоку, я думаю. –

1

С substr возвращает std::string, нет способа вернуть прокси-объект, и они не могут просто изменить тип возврата или перегрузить его (по причинам, упомянутым вами).

Они могут сделать это, сделав string сам способным быть вспомогательным устройством другой строки. Это означало бы штраф за память для всех обычаев (для хранения дополнительной строки и двух типов size_types). Кроме того, каждая операция должна проверять, есть ли у нее символы или является прокси. Возможно, это может быть сделано с указателем реализации - проблема в том, что теперь мы делаем класс общего назначения более медленным для возможного случая кромки.

Если вам это нужно, лучший способ - создать еще один класс, substring, который строит из строки, pos и длины и скрывает строку. Вы не можете использовать его как s1.substr(6), но вы можете сделать

substring sub(s1, 6); 

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

0

Что касается вашего конкретного примера, это работает для меня:

if (&s1[6] == s2) { 
    std::cout << "match!" << std::endl; 
} 

Это не может ответить на ваш вопрос, для решения общего назначения. Для этого вам понадобится подстрока CoW, как предлагает @GMan.

3

Это довольно известная оптимизация, которая относительно широко используется, называется copy-on-write или COW. Основная вещь даже не делать с подстроки, но с чем-то же просто, как

s1 = s2; 

Теперь, проблема с этой оптимизации является то, что для библиотек C++, которые должны быть использованы на цели, поддерживающих несколько потоков, ссылка счетчик для строки должен быть доступен с помощью атомных операций (или, что еще хуже, защищен мьютексом, если целевая платформа не предоставляет атомные операции). Это достаточно дорого, что в большинстве случаев простая реализация строки, отличной от COW, выполняется быстрее.

См GOTW # 43-45:

http://www.gotw.ca/gotw/043.htm

http://www.gotw.ca/gotw/044.htm

http://www.gotw.ca/gotw/045.htm

В довершение, библиотеки, которые используют коровье, такие как библиотеки GNU C++, не может просто вернемся к простой реализации, так как это нарушит ABI. (Несмотря на то, что C++ 0x на помощь, так как для этого потребуется авария ABI в любом случае! :)

0

Что вы имеете в виду (или было) одной из основных особенностей класса Java java.lang.String (http://fishbowl.pastiche.org/2005/04/27/the_string_memory_gotcha/). Во многих отношениях конструкции классов String Java и шаблона C# basic_string аналогичны, поэтому я бы предположил, что возможность создания шаблона с использованием этой «оптимизации подстроки» возможна.

Одна вещь, которую вам нужно будет рассмотреть, - как написать реализацию члена c_str() const. В зависимости от расположения строки в качестве подстроки другого, возможно, придется создать новую копию.Это определенно должно было бы создать новую копию внутреннего массива, если строка, для которой была запрошена c_str, не является конечной подстрокой. Я думаю, что это требует использования ключевого слова mutable большинства, если не всех, элементов данных реализации basic_string, что значительно усложняет реализацию других методов const, потому что компилятор больше не может помочь программисту с корректностью const.

EDIT: На самом деле, для размещения c_str() const и data() const, вы можете использовать один изменяемое поле типа const charT*. Первоначально, установленный в NULL, он может быть для каждого экземпляра, инициализирован указателем на новый массив charT всякий раз, когда вызывается c_str() const или data() const и удаляется в деструкторе basic_string, если не NULL.

0

Если вам действительно нужно больше производительности, чем std :: string, то запустите и напишите что-нибудь, что работает так, как вам нужно. Раньше я работал с вариантами строк.

Мое собственное предпочтение заключается в том, чтобы использовать не изменяемые строки, а не copy-on-write, и использовать boost :: shared_ptr или эквивалент, но только тогда, когда длина строки превышает 16, поэтому класс string также имеет частный буфер для коротких строк.

Это означает, что класс строк может нести немного веса.

У меня также есть в моем списке коллекции класс «срез», который может смотреть на «подмножество» класса, который живет где-то еще до тех пор, пока время жизни исходного объекта не повреждено. Поэтому в вашем случае я мог бы нарезать строку, чтобы увидеть подстроку. Конечно, это не было бы завершено нулем, и нет никакого способа сделать это таким, не копируя его. И это не класс строк.

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