Здравствуйте, all.
Долго пытался не влезать в эту дискуссию, но все же попытаюсь изложить собственное мнение.
(Ибо пятница, вечер и лениво делать что-то умное. К тому же ребилд %)).
Читая эту уже порядком затянувшуюся ветку, все больше и больше убеждаюсь, что
все "Минусы STL" и "Проблемы STL контейнеров" кроются в закостенелости мышления.
Большинство упреков (есть, конечно, и минусы, без этого нельзя) появляются
вследствие не(до)понимания каких-то концепций.
К сожалению, это характерно не только для новичков, но и для маститых программистов
которые уже привыкли всюду писать свои велосипеды и не доверяют стандартной библиотеке.
Это также объясняется тем, что некоторые воспринимают C++ только как синтаксическое
расширение C, т.е. не принимают (не понимают или даже не пытаются понять) тех парадигм,
которые делают С++ высокоуровневым языком.
Да, можно успешно писать на С++ не используя стандартную библиотеку,
ровно как писать на Java и использовать собственные контейнеры и алгоритмы.
Но осмысленно ли это?
Те, кто говорят, что «STL плоха, ибо я ее не использую», скорей всего просто не разобрались STL.
Те, кто говорят, что «STL не юзабельная», просто не видели больших проектов, построенных с использованием STL.
(В продукте над которым я работаю около 200 проектов. Все построены на COM + STL. Как это не удивительно, но
юзать MFC никому даже в голову не пришло %)).
Те же, кто говорят, что STL бедна, ибо не реализует B-деревья, топологическую сортировку,
фибоначиву кучу, алгоритмы Дейкстры, Форда-Фалкерсона, Джонсона…, не решает задачи
о рюкзаке, коммивояжере, максимальном потоке…, не предоставляет средства для работы с сокетами,
USB, сотовыми телефонами, Wi-Fi и т.д. требуют от стандартной библиотеки слишком много.
Некоторые думают, что STL жутко тормозная и экономят каждую инструкцию в тех местах, где это никому
не нужно (пресловутая преждевременная оптимизация). К примеру, в GUI.
В действительности, даже такие структуры как std::map<string> достаточно быстры,
что позволяют писать высокоточные профайлеры.
Ну, а те, кого не интересует мнения классиков, обречены вечно наступать на свои же грабли.
Ибо, как известно, дурак учится на своих ошибках, а умный на чужих %).
К сожалению, такой порядок дел не изменить в силу все той же закостенелости мышления и нежелании что-либо менять.
Поэтому, ИМХО, данная ветка уже давно перешла в ранг холиворов вроде C vs C++.
Остается только надеяться, что такое положение дел положительно скажется на развитии стандартной библиотеки.
--
С уважением, Александр
Здравствуйте, Alex_Avr, Вы писали:
A_A>Здравствуйте, Аноним, Вы писали:
А>>3. Нет понятия 'NULL-итератор'. В качестве итератора, "ссылающегося в никуда", обычно используют Container.end(). Но вместо кода:
А>>А>>if (m_itSelectedItem != m_pServer->ItemsEnd())
А>>{
А>> m_pServer->ReleaseItem(m_itSelectedItem);
А>> m_itSelectedItem = m_pServer->ItemsEnd();
А>>}
А>>
А>>было бы гораздо удобнее писать:
А>>А>>if (m_itSelectedItem != NULL) // NULL-итератор
А>>{
А>> m_pServer->ReleaseItem(m_itSelectedItem);
А>> m_itSelectedItem = NULL; // NULL-итератор
А>>}
А>>
А>>Особенно, если такого кода много. Вариант с NULL-итератором лучше по всем показателям:
А>>
A_A>Как-то не было проблем с чтением.
'm_pServer->ItemsEnd()' — три слова.
'NULL' — одно слово.
Я вариант с 'NULL' читаю быстрее. Когда вижу слово 'NULL', в голове сразу же появляется мысль: "нет объекта". Нужно чуть больше времени, чтобы понять, что такое 'm_pServer->ItemsEnd()'.
А>>
A_A>В современных IDE разницы практически нет.
В современных IDE программисты часто ускоряют написание ' != NULL'. А вот ускорить написание ' != m_pServer->ItemsEnd()' сложнее из-за большого количества вариантов.
А>>
А>>Порождает меньше машинных инструкций.
A_A>Не факт, компилятор может встроить тело end () в место вызова.
Я проверил в MSVC6 Release. Контейнер — list. Вариант с end-итератором:
void Client::ReleaseRecentItem()
{
004014F0 push esi
004014F1 mov esi,ecx
if (m_itRecentItem != m_pServer->ItemsEnd())
004014F3 mov ecx,dword ptr [esi]
004014F5 mov eax,dword ptr [esi+4]
004014F8 cmp eax,dword ptr [ecx+4]
004014FB je Client::ReleaseRecentItem+1Dh (0040150d)
{
m_pServer->ReleaseItem(m_itRecentItem);
004014FD mov edx,eax
004014FF push edx
00401500 call Server::ReleaseItem (004040c0)
m_itRecentItem = m_pServer->ItemsEnd();
00401505 mov eax,dword ptr [esi]
00401507 mov ecx,dword ptr [eax+4]
0040150A mov dword ptr [esi+4],ecx
0040150D pop esi
}
}
0040150E ret
--- No source file ------------------------------------------------------------
0040150F nop
Компилятор действительно встроил оба вызова Server::ItemsEnd (возвращает 'm_Items.end()'). Размер функции: 0x0040150F — 0x004014F0 = 31 байт. Вариант с NULL-итератором:
void Client::ReleaseRecentItem()
{
004014F0 push esi
004014F1 mov esi,ecx
if (m_itRecentItem != NULL)
004014F3 mov eax,dword ptr [esi+4]
004014F6 test eax,eax
004014F8 je Client::ReleaseRecentItem+19h (00401509)
{
m_pServer->ReleaseItem(m_itRecentItem);
004014FA mov ecx,dword ptr [esi]
004014FC push eax
004014FD call Server::ReleaseItem (004040c0)
m_itRecentItem = NULL;
00401502 mov dword ptr [esi+4],0
00401509 pop esi
}
}
0040150A ret
--- No source file ------------------------------------------------------------
0040150B nop
Размер функции: 0x0040150B — 0x004014F0 = 27 байт. Разница действительно мизерная (4 байта). Время работы, скорее всего, тоже почти одинаково (не мерял). Но в данном случае размер ассемблерного кода и скорость работы на втором плане. В первую очередь, NULL-итераторы дают:
Возможность инициализировать итератор без доступа к контейнеру. Неинициализированный итератор опасен. Если его случайно использовать, то есть риск тихо испортить память.
Улучшение читаемости кода.
A_A>Итераторы могут быть классами, а не простыми указателями (как в STLPort в отладочном билде), в этом случае такой поход работать не будет.
Обычно итераторы — это обёртки над указателями. Поэтому легко добавить поддержку NULL-итераторов. Например, для list-а:
template<typename TElem, ...>
class list
{
...
struct Node
{
Node* pPrev;
Node* pNext;
TElem Elem;
};
...
class iterator
{
Node* m_pNode;
public:
iterator()
{
}
// NULL-итератор
iterator(int NullTag)
{
assert(NullTag == 0);
m_pNode = NULL;
}
TElem& operator*() const
{
return m_pNode->Elem;
}
TElem* operator->() const
{
return &m_pNode->Elem;
}
iterator& operator--()
{
m_pNode = m_pNode->pPrev;
return *this;
}
iterator& operator++()
{
m_pNode = m_pNode->pNext;
return *this;
}
iterator operator--(int PostfixTag)
{
iterator it = *this;
operator--();
return it;
}
iterator operator++(int PostfixTag)
{
iterator it = *this;
operator++();
return it;
}
friend bool operator==(iterator it1, iterator it2)
{
return it1.m_pNode == it2.m_pNode;
}
friend bool operator!=(iterator it1, iterator it2)
{
return it1.m_pNode != it2.m_pNode;
}
};
...
};
Такой подход работает уже 8 лет, так как MSVC6 вышел в 98-ом году:
list<int>::iterator it1 = NULL; // итератор list-а - класс
assert(it1 == NULL);
assert(!(it1 != NULL));
map<int, int>::iterator it2 = NULL; // итератор map-а - класс
assert(it2 == NULL);
assert(!(it2 != NULL));
А>>5. Нельзя получить итератор по указателю на элемент контейнера за константное время.
A_A><код поскипан>
А>>Желательно, чтобы код, выделенный жирным, работал за константное время.
A_A>А кто сказал, что это возможно для любых итераторов и любых контейнеров?
Никто. Но для некоторых возможно:
Для vector-а это не нужно (realloc аннулирует указатель на элемент), но можно сделать уже сейчас:
int Index = pElem - &Vec[0];
vector<...>::iterator itElem = Vec.begin() + Index;
assert(&*itElem == pElem);
Для vector<bool> нельзя получить указатель на элемент.
Для node-based контейнеров (list, map) итератор — это обёртка над Node*. Адреса pNodе и pElem различаются на константу (смещение Elem в структре Node). Поэтому можно сделать быстрый iterator_from_pointer.
Для deque нельзя получить итератор по указателю на элемент за константное время. deque — это не node-based контейнер, а массив указателей на массивы.
Для hash_map зависит от того, как он сделан. Обычно hash_map содержит массив "вёдер" (bucket). В каждом ведре — список элементов, чьё hash-значение равно индексу ведра. Поэтому индекс ведра можно восстановить по элементу (вычислив hash-значение). Например, в SGI STL (файл stl_hashtable.h):
template <class _Val>
struct _Hashtable_node
{
_Hashtable_node* _M_next;
_Val _M_val;
};
_M_val — это элемент hash-таблицы (для hash_map — pair(const key, data)). _M_next — это указатель на следующий node в ведре. Там же, далее:
template <class _Val, ...>
struct _Hashtable_iterator {
typedef hashtable<_Val, ...> _Hashtable;
typedef _Hashtable_iterator<_Val, ...> iterator;
...
typedef _Hashtable_node<_Val> _Node;
...
typedef _Val& reference;
...
_Node* _M_cur;
_Hashtable* _M_ht;
...
reference operator*() const { return _M_cur->_M_val; }
...
iterator& operator++();
...
};
Итератор содержит указатель на hash-таблицу (_M_ht) и указатель на node (_M_cur). Других значений нет. После последнего node-а в ведре нужно перейти к следующему ведру. Итератор восстанавливает индекс ведра по элементу (вычисляя hash-значение):
template <class _Val, ...>
_Hashtable_iterator<_Val, ...>& _Hashtable_iterator<_Val, ...>::operator++()
{
const _Node* __old = _M_cur;
_M_cur = _M_cur->_M_next;
if (!_M_cur) {
size_type __bucket = _M_ht->_M_bkt_num(__old->_M_val);
while (!_M_cur && ++__bucket < _M_ht->_M_buckets.size())
_M_cur = _M_ht->_M_buckets[__bucket];
}
return *this;
}
При переходе к следующему ведру итератор использует некоторые члены hash-таблицы. _M_buckets — это vector<_Node*, ...> (массив вёдер). _M_bkt_num — это функция, вычисляющая hash-значение (обёртка над функциональным объектом _M_hash). Для такой реализации можно сделать быстрый iterator_from_pointer:
template<class _Val, ...>
class hashtable
{
...
typedef _Hashtable_node<_Val> _Node;
...
typedef _Hashtable_iterator<_Val, ...> iterator;
...
iterator iterator_from_pointer(_Val* pVal)
{
iterator it;
it._M_ht = this;
it._M_cur = reinterpret_cast<_Node*>(reinterpret_cast<Byte*>(pVal) - offsetof(_Node, _M_val));
assert(&it._M_cur->_M_val == pVal);
return it;
}
};
Здесь не нужно восстанавливать индекс ведра, потому что итератор его не хранит (возможно, чтобы при resize-е hash-таблицы существующие итераторы остались в силе). Если бы итератор хранил индекс ведра, я бы добавил строчку:
it._M_bucket = _M_bkt_num(*pVal); // вычислить hash-значение
С натяжкой можно сказать, что это работает за константное время (по крайней мере не зависит от количества элементов в hash-таблице).
В STLport 4.5.3 то же самое (она основана на SGI STL). Там есть одна интересная деталь: hash-таблица использует что-то вроде vector<void*> для массива вёдер, чтобы уменьшить разбухание (ассемблерного) кода (code bloat).
Можно не вычислять hash-значение при переходе к следующему ведру, а хранить hash-значения в node-ах. Так делает CMap в MFC 4.2:
template<class KEY, class ARG_KEY, class VALUE, class ARG_VALUE>
class CMap : public CObject
{
protected:
// Association
struct CAssoc
{
CAssoc* pNext;
UINT nHashValue; // needed for efficient iteration
KEY key;
VALUE value;
};
...
};
Можно поступить так же, как с list::size: стандарт рекомендует, чтобы iterator_from_pointer работал за константное время, но реализация может работать за время O(N) (N — количество элементов в контейнере).
А>>9. list
А>>9.1. list спроектирован так, что либо size за константное время, либо splice (вариант с 4-мя параметрами) за константное время (при условии, что allocator-ы равны). Стандарт рекомендует ("should"), чтобы list::size работал за константное время, но выбор оставлен на усмотрение реализации STL. Соответственно, есть "странные" реализации STL (например, SGI STL, раздел "Why is list<>::size() linear time?"), в которых size вычисляется, а не хранится. То есть реализация size-а пробегает по всему списку и таким образом узнаёт количество элементов.
А>>В MFC CList::GetCount работает за константное время:
А>>А>>
А>>template<class TYPE, class ARG_TYPE>
А>>AFX_INLINE int CList<TYPE, ARG_TYPE>::GetCount() const
А>> { return m_nCount; }
А>>
А>>О проблеме писали здесьАвтор: Sergeem
Дата: 26.05.03
, здесьАвтор: Artour A. Bakiev
Дата: 18.01.04
.
A_A>То, что в одной из реализаций STL list::size () имеет линейную сложность еще ни очем не говорит.
Это говорит о том, что стоит осторожно обращаться с list::size. Например, выносить за тело цикла, если список не меняется в ходе цикла. Если меняется, то можно запросить list::size один раз до цикла, а в ходе цикла вручную отслеживать длину списка. Правильность ручной длины списка можно проверять assert-ом.
Кстати, STLport 4.5.3 тоже "странная" (файл _list.h):
template <...>
class list ... {
...
size_type size() const {
size_type __result = distance(begin(), end());
return __result;
}
...
};
A_A>Да и есть ли в CList аналог list::splice ()?
Нет. В MFC 4.2 каждый CList имеет личный менеджер памяти (pool). Поэтому нельзя дёшево перетащить элементы из одного списка в другой. Можно только дорого:
template<...>
void CList<...>::AddHead(CList* pNewList)
{
...
// add a list of same elements to head (maintain order)
POSITION pos = pNewList->GetTailPosition();
while (pos != NULL)
AddHead(pNewList->GetPrev(pos));
}
template<...>
void CList<...>::AddTail(CList* pNewList)
{
...
// add a list of same elements
POSITION pos = pNewList->GetHeadPosition();
while (pos != NULL)
AddTail(pNewList->GetNext(pos));
}
А>>9.2. Неизвестен порядок уничтожения элементов в деструкторе list-а. Специализированный менеджер памяти, работающий как stack, может требовать, чтобы блоки памяти освобождались в обратном порядке. Я проверил две реализации STL: MSVC6 STL и Rogue Wave STL 2.1.1 (поставляется вместе с BCB5). В обеих ~list уничтожает элементы в прямом порядке. То есть если я наполняю list push_back-ами, то stack-овый менеджер памяти использовать нельзя.
A_A>А когда это нужно?
Например, есть кратковременный список, живущий в блоке кода. Если функция вызывается часто (например, при resize-е окна), то желательно оптимизировать распределение памяти. Можно использовать alloca, но это опасно (риск получить stack overflow). Лучше использовать stack-овый менеджер памяти. Это редкий случай, когда оптимизация почти не усложняет код. Вместо:
typedef list<Item> ItemList;
ItemList Items;
достаточно было бы написать:
typedef list<Item, TempAllocator<Item> > ItemList;
ItemList Items;
A_A>На мой вгляд, закладываться на особенности внутренней реализации контейнера не есть правильно.
Теоретически, ~list может уничтожать элементы в случайном порядке:
template<typename TElem, ...>
list<TElem, ...>::~list()
{
while (!empty())
{
int Index = rand() % size();
iterator it = begin();
advance(it, Index);
erase(it);
}
// что-нибудь ещё
...
}
Фактически, выбор маленький: уничтожать элементы в прямом порядке или в обратном. Гарантия на этот счёт могла бы помочь при оптимизации.
А>>9.3. push_front и push_back не возвращают итератор на ново-вставленный элемент. Мелочь, а неудобно. В будущем это вполне можно исправить, старый код менять не придётся. Пока, можно сделать обёртку над list-ом:
А>>А>>template<typename TElem>
А>>class MyList : public list<TElem>
А>>{
А>>public:
А>> typename iterator push_front(const TElem& e)
А>> {
А>> list<TElem>::push_front(e);
А>> return begin();
А>> }
А>> typename iterator push_back(const TElem& e)
А>> {
А>> list<TElem>::push_back(e);
А>> typename iterator it = end();
А>> return --it;
А>> }
А>>};
А>>
A_A>Гм, а почему нельзя просто использовать list::front () и list::back ()?
front/back возвращают
ссылку на элемент. Иногда нужно добавить элемент в конец списка и получить
итератор на ново-вставленный элемент. Например,
здесьАвтор: Пётр Седов
Дата: 07.09.06
я писал:
class Item
{
...
explicit Item(const string& Name);
};
class ItemManager
{
public:
typedef list<Item>::iterator ItemHandle; // ...
ItemHandle AddItem(const string& Name); // ...
...
list<Item> m_Items;
};
ItemManager::ItemHandle ItemManager::AddItem(const string& Name)
{
// push_back не возвращает итератор
return m_Items.insert(m_Items.end(), Item(Name));
}
A_A>Мне кажется, что операции получения итераторов на элементы и вставка разделены не просто так,
Разделены? list::insert, вставляющий один элемент, возвращает итератор на ново-вставленный элемент. Чем push_front/push_back хуже?
A_A>а для того, чтобы можно было гарантировать предсказуемое состояние
A_A>программы в случае, если итератор является классом и при его копировании
A_A>может вылететь исключение:
Итератор list-а — это всегда класс. Но это всего лишь тонкая обёртка над указателем на node.
A_A>
A_A>class A { int n; };
A_A>list<A> list_of_A;
A_A>list<A>::iterator it = list_of_A.push_back (A ()); // если здесь вылетело исключение о нехватки памяти, как
A_A> // определить произошло это при вставке элемента в контейнер
A_A> // или при копировании итератора? И в каком состоянии
A_A> // теперь находится list_of_A, был ли в него добавлен
A_A> // элемент?
A_A>
Вряд ли конструктор копирования итератора бросает исключение о нехватке памяти. Зачем итератору запрашивать память? А вот allocator::allocate может бросить исключение о нехватке памяти. В этом случае состояние list_of_A не изменится (список останется пустым).
Обычно итераторы копируются дёшево и без исключений. Если методы итератора и бросают исключения, то это скорее что-нибудь вроде logic_error (а не bad_alloc). Например, усердная реализация STL может ловить сравнение итераторов от разных list-ов (это логическая ошибка).
А>>10. string
А>>10.1. Если программа много работает с текстом, то string (или его аналог) — ходовой тип. При этом желательно, чтобы неявное копирование string-а было дешёвым. Для этого реализация string-а может использовать разделяемый (между несколькими string-ами) буфер с подсчётом ссылок.
A_A>Сейчас , на сколько я знаю, отходят от использования COW при реализации std::string из-за проблем с многопоточностью,
Многие программы однопоточны. Поэтому хорошо бы иметь выбор:
// заголовочный файл <string>
#ifdef _MT // = multi thread
// многопоточная реализация basic_string-а (не считает ссылки)
#else
// однопоточная реализация basic_string-а (считает ссылки)
#endif
Я не люблю, когда такой выбор сделан за меня.
A_A>к тому же это только одна из возможных реализаций.
По большому счёту все реализации string-а делятся на две группы:
С подсчётом ссылок.
Без подсчёта ссылок, vector-стиль. Реализация может использовать фиксированный буфер для коротких последовательностей, но для длинных она динамически запрашивает память. При этом неявное копирование дорого и может бросить исключение (если не хватит памяти). Это не очень хорошо, если string — элемент STL-контейнера (например, list<string>). Страуструп в книге "Язык программирования C++" (третье издание) в разделе 17.1.4 ("Требования к элементам" (STL-контейнеров)) пишет по этому поводу:
Например, операция копирования, генерирующая исключения, может оставить какой-нибудь элемент лишь частично скопированным. Она может оставить даже сам контейнер в состоянии, которое потом принесёт неприятности. Такие операции копирования сами по себе являются плохо спроектированными (§ 14.4.6.1).
То есть желательно, чтобы неявное копирование либо было запрещено, либо было дёшево. Дорогое неявное копирование подозрительно.
A_A>Все-таки как-то странно подгонять интерфейс под реализацию.
В том то и дело, что интерфейс string-а подогнан под реализацию в vector-стиле (без подсчёта ссылок). Есть "плохие" методы, возвращающие не-const ссылку на буфер (например, не-const operator[]). Если реализация считает ссылки, то внешняя не-const ссылка на буфер сильно ограничивает свободу string-а, он вынужден уважать эту ссылку до realloc-а или до деструктора. "Плохой" метод делает copy-on-write (возможно, холостой) и жёстко привязывает буфер к одному единственному string-у. Привязанный буфер не может разделяться между несколькими string-ами. При этом реализация деградирует до vector-стиля и неявное копирование дорожает. К сожалению, интерфейс string-а недостаточно отделён от реализации, он ставит палки в колёса реализации, считающей ссылки.
У MFC-шного CString-а тоже есть методы, возвращающие не-const ссылку на буфер. Но. Во-первых, очень тяжело
случайно вызвать методы GetBuffer, GetBufferSetLength и LockBuffer (в отличие от не-const string::operator[]). Во-вторых, есть парные методы (ReleaseBuffer и UnlockBuffer), аннулирующие ранее выданный указатель. Такой подход более дружелюбен к подсчёту ссылок (в MFC 4.2 CString считает ссылки).
A_A>С другой стороны, может было бы лучше как в Java реализовать отдельно классы для неизменяемых строк и строк с возможностью изменения.
Конечно! Разумно разделить класс string на два: string2 и string2_builder, которому и передать всё изобилие методов append, insert, erase и replace; можно ещё добавить prepend. У string2 только базовые способы наполнения: конструкторы, assign и прямой доступ к буферу на запись.
А>>10.2. Нет завершающего '\0'. Вот такой код:
А>>А>>string Text = "abc";
А>>assert(Text[3] == '\0');
А>>
А>>содержит логическую ошибку, так как '3' является ошибочным индексом для string-а с length = 3. Усердная реализация STL, проверяющая индекс, обнаружит это и сообщит об ошибке (с помощью assert-а или исключения). Если реализация STL беспечная (не проверяет индекс), то фактически код будет работать, так как '\0' в конец обычно ставят (чтобы иметь быструю реализацию метода c_str).
A_A>Используйте правильную STL
A_A>Вообще-то не понял, в чем проблема. Насколько я знаю, в string обычно хранится именно NULL terminating строка.
A_A>Где это не так?
Это везде так, но официально доступ к завершающему '\0' запрещён. Усердная реализация operator[], проверяющая индекс, воспримет доступ к завершающему '\0' как ошибку. Это повлечёт сообщение от assert-а или исключение (вроде out_of_range).
А>>Иногда завершающий '\0' удобен при parsing-е.
A_A>Парсинг это частная задача и подгонять std::string под вашу конкретную его реализацию несколько станно.
При проектировании строкового класса
очень разумно учитывать потребности parsing-а. Подгонять ничего не надо: достаточно официально разрешить доступ к завершающему '\0'.
A_A>Целесообразнее, IMHO, парсинг проводить с помощью алгоритмов, регулярных выражений или конечных автоматов.
Или с помощью готовых parser-ов (например, XML-parser). Можно использовать генераторы parser-ов (например, YACC, Lex). Но иногда это стрельба из пушки по воробьям. В простых случаях (как IsValidMethodName) лучше не усложнять код.
А>>В будущем доступ к завершающему '\0' может быть разрешён.
A_A>Ага, что бы пользователь класса сам мог этот нуль затереть и получить AV или SEGFAULT при вызове std::string::find например
Последовательность char-ов string-а может содержать '\0'. Поэтому, в отличие от strchr, string::find не использует завершающий '\0' как барьер, а честно проверяет граничное условие. Затерев завершающий '\0', программист повредит только себе: c_str будет возвращать указатель на буфер, не завершающийся '\0'.
В идеале, хорошо бы чётко разделить чтение и запись:
template<typename TChar, ...>
class basic_string
{
...
// можно читать завершающий '\0'
TChar operator[](size_type Index) const
{
assert(Index <= length());
...
}
// нельзя писать в завершающий '\0'
void set_at(size_type Index, TChar c)
{
assert(Index < length());
...
}
...
};
А>>10.3. Нет прямого доступа к буферу на запись.
A_A>Это позволяет string-у самому управляться со своими данными, что предотвращает проблемы некорректного с ними отношения со стороны пользователя.
Прямой доступ к буферу на запись — низкоуровневая возможность. Она позволяет быстро наполнить string без промежуточного буфера, в том числе с помощью C-style API. Эта возможность нужна в первую очередь создателям высокоуровневых библиотек для эффективной реализации. Новички таким не занимаются, они используют эти самые высокоуровневые библиотеки.
А защищаться от ошибочного использования надо не урезанием функциональности, а отладочными проверками.
А>>А>>string Text;
А>>int Len = ::GetWindowTextLength(hEditControl);
А>>if (Len != 0)
А>>{
А>> Text.resize(Len);
А>> ::GetWindowText(hEditControl, &Text[0], Len + 1);
А>>}
А>>
A_A>Ну да, а если пользователь забыл вызвать resize () или указал недостаточный размер?
Отсутствие прямого доступа к буферу на запись не защищает от такой ошибки. С промежуточным буфером точно так же есть риск указать недостаточную длину:
string Text;
int Len = ::GetWindowTextLength(hEditControl);
if (Len != 0)
{
vector<char> Buf(Len); // программист забыл '+ 1' для завершающего '\0'
::GetWindowText(hEditControl, &Buf[0], Len + 1);
Text.assign(&Buf[0], Len);
}
A_A>Кроме того, как тогда быть с выделенным:
А>>Фактически это работает, но стандарт не гарантирует, что string хранит char-ы последовательно в одном массиве (кстати, для vector-а такая гарантия есть (не считая vector<bool>)). Теоретически, string может хранить char-ы задом наперёд или в нескольких массивах (как deque).
Такая возможность дана string-у для оптимизации (например, конкатенации). Но для оптимизации нужно знать особенности программы. string их не знает, это универсальный класс. Поэтому все известные мне реализации string-а не выпендриваются и хранят char-ы в одном монолитном буфере. Реализацию string-а в deque-стиле я видел только на картинке в книге Страуструпа "Язык программирования C++" (третье издание) в разделе 17.1.3 ("Представление" (STL-контейнеров)). Даже если доведётся использовать такую реализацию, то можно выбрать вариант GENERIC_STL_IMPL (с промежуточным буфером). Но скорее всего не доведётся, так как есть движение в сторону гарантии непрерывности char-ов в string-е.
Кстати, конкатенацию строк можно оптимизировать гораздо проще, заменив бинарный operator+ на "сколько-угодно-арную" функцию Concat. Об этом — пункт 10.4 (ниже).
А>>В будущем прямой доступ к буферу на запись может быть добавлен. Пока, проблему можно решить, введя дополнительный слой абстракции:
A_A><код поскипан>
А>>Настройками компиляции можно выбрать "законный" или быстрый вариант, не меняя код.
A_A>А если в одном месте кода нужно быстро, а в другом месте безопасно?
Вряд ли есть проект, в котором один класс использует MSVC6 STL, а другой — STLport. В большинстве проектов весь код использует одну и ту же реализацию STL. То, как реализован string, и определяет выбор между GENERIC_STL_IMPL и REALISTIC_STL_IMPL. Вариант REALISTIC_STL_IMPL (без промежуточного буфера) вполне безопасный и работает со всеми известными мне реализациями STL.
A_A>Лучше использовать отдельный класс для манипуляций со строками.
Да. Создатели крупных библиотек (MFC, Qt, wxWidgets, VCL) так и делают. Часто их "велосипедные" строковые классы эффективнее стандартного.
А>>10.4. Конкатенация строк
А>>Вместо бинарного operator+, сцепляющего строки по очереди, было бы оптимальнее (по скорости работы программы) использовать функцию Concat, сцепляющую несколько строк одним махом.
A_A>То же самое, для критичного по производительности кода лучше использовать специализированный класс для манипуляций со строками.
Создатель библиотеки не может точно узнать, где критичный по скорости код (а string предназначался в первую очередь для библиотек, как стандартная инфраструктура). Если пользователь редко вызывает функцию, то она не критична по скорости. Но другой пользователь может вызывать ту же функцию очень часто, тогда она станет критичной по скорости. В таких условиях лучше попусту не терять эффективность из-за неудачно спроектированного string-а. Поэтому создатели крупных библиотек делают собственные строковые классы. Создатели библиотек поменьше (например, WTL) перенимают удачные решения.
А>>В функции DoConcat можно использовать класс ostringstream в качестве string builder-а.
A_A>А вот это точно не стоит использовать в критичном по времени коде.
Допустим, я пишу переносимую библиотеку и мне нужно вернуть string из функции. Я знаю только два стандартных string builder-а:
Собственно, сам string.
ostringstream.
Разве есть ещё?
По поводу медленности iostreams согласен полностью.
А>>11. У priority_queue нет метода increase_priority(element) (чтобы ускорить выталкивание элемента из очереди). Поэтому priority_queue нельзя использовать в алгоритме Дейкстры и A*. Я не знаю, как устроена структура данных 'пирамида' (heap, частично упорядоченное сортирующее дерево), так что не могу сказать, можно ли безболезненно добавить increase_priority.
A_A>Скорее всего, increase_priority придется сначала находить нужный элемент, удалять его и вставлять повторно с новым приоритетом.
Возможно, я хочу слишком много от priority_queue. Этот пункт можно вычеркнуть из "Проблем STL-контейнеров" (тем более что priority_queue — не контейнер, а всего лишь адаптер контейнера). В конце концов, есть
Boost Graph Library, сделанная в STL-стиле.
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
довльно неудобно, что *std::map::iterator ссылается на std::pair<key, value>, а не на само value, в связи с этим очнь сложно использовать её со стандартными алгоритмами и биндом, благо в boost::ptr_map этого недостатка нет.