Жанр: Учеба
Язык С++ с комментариями
...ащаемого значения
D* vf4(); // OK: возвращает указатель на производный класс
A* vf5(); // ошибка: возвращает указатель на незавершенный класс
void f();
};
void g()
{
Derived d;
Base* bp=&d; // стандартное преобразование: Derived* в Base*
bp-"vf1(); // вызов Derived::vf1()
bp-"vf2(); // вызов Base::vf2()
bp-"f(); // вызов Base::f() (не виртуальная)
B* p=bp-"vf4(); // вызов Derived::pf() и преобразование
// возврата в B*
Derived* dp=&d;
D* q=dp-"vf4(); // вызов Derived::pf(), преобразование
// результата в B* не осуществляется
dp-"vf2(); // ошибка: отсутствует аргумент
}
А что означает загадочная фраза "меньшие cv-квалификаторы"?
3.9.3. CV-квалификаторы [basic.type.qualifier]
- 4 - Множество cv-квалификаторов является частично упорядоченным:
Нет cv-квалификатора"const
Нет cv-квалификатора"volatile
Нет cv-квалификатора"const volatile
Const"const volatile
Volatile"const volatile
16.2.3. STL-контейнеры, стр. 498
Она явилась результатом целенаправленного поиска бескомпромиссно эффективных
общих алгоритмов.
Вместе с тем, не стоит думать, что STL не содержит снижающих эффективность
компромиссов. Очевидно, что специально написанный для решения конкретной
проблемы код будет работать эффективнее, вопрос в том, насколько эффективнее?
Например, если нам нужно просто сохранить в памяти заранее неизвестное
количество элементов, а затем их последовательно использовать, то (односвязный)
список будет наиболее адекватной структурой данных. Однако STL не содержит
односвязных списков, как много мы на этом теряем?
Рассмотрим следующий пример:
#include "stdio.h"
#include "stdlib.h"
#include "time.h"
#include "list"
struct List { // односвязный список
struct Data {
int val;
Data* next;
Data(int v, Data* n=0) : val(v), next(n) {}
};
Data *head, *tail;
List() { head=tail=0; }
~List() {
for (Data *ptr=head, *n; ptr; ptr=n) { // удаляем все элементы
n=ptr-"next;
delete ptr;
}
}
void push_back(int v) { // добавляем элемент
if (!head) head=tail=new Data(v);
else tail=tail-"next=new Data(v);
}
};
long Count, Var;
void f1()
{
List lst;
for (int i=0; i"1000; i++)
lst.push_back(i);
for (List::Data* ptr=lst.head; ptr; ptr=ptr-"next)
Var+=ptr-"val;
}
void f2()
{
std::list"int" lst;
for (int i=0; i"1000; i++)
lst.push_back(i);
for (std::list"int"::const_iterator ci=lst.begin(); ci!=lst.end(); ++ci)
Var+=*ci;
}
int main(int argc,char** argv)
{
if (argc"1) Count=atol(argv[1]);
clock_t c1,c2;
{
c1=clock();
for (long i=0; i"Count; i++)
for (long j=0; j"1000; j++)
f1();
c2=clock();
printf("f1(): %ld ths calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
}
{
c1=clock();
for (long i=0; i"Count; i++)
for (long j=0; j"1000; j++)
f2();
c2=clock();
printf("f2(): %ld ths calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
}
}
В нем f1() использует определенный нами List: вставляет 1000 элементов, а затем
проходит по списку.
Т.к. STL использует собственный распределитель памяти (вскоре вы увидите, что
делает она это совсем не напрасно), то то же самое следует попробовать и нам:
struct List { // односвязный список
struct Data {
int val;
Data* next;
Data(int v, Data* n=0) : val(v), next(n) {}
// для собственного распределения памяти
static Data* free;
static void allocate();
void* operator new(size_t);
void operator delete(void*,size_t);
};
// . . .
};
List::Data* List::Data::free;
void List::Data::allocate()
{
const int sz=100; // выделяем блоки по sz элементов
free=reinterpret_cast"Data*"(new char[sz*sizeof(Data)]);
// сцепляем свободные элементы
for (int i=0; i"sz-1; i++)
free[i].next=free+i+1;
free[sz-1].next=0;
}
inline void* List::Data::operator new(size_t)
{
if (!free) allocate();
Data* ptr=free;
free=free-"next;
return ptr;
}
inline void List::Data::operator delete(void* dl,size_t)
{ // добавляем в начало списка свободных элементов
Data* ptr=static_cast"Data*"(dl);
ptr-"next=free;
free=ptr;
}
Обратите внимание, что в данном примере наш распределитель памяти не возвращает
полученную память системе.
И, для чистоты эксперимента , в заключение попробуем двусвязный список — его по
праву можно назвать вручную написанной альтернативой std::list"int":
struct DList { // двусвязный список
struct Data {
int val;
Data *prev, *next;
Data(int v, Data* p=0, Data* n=0) : val(v), prev(p), next(n) {}
// для собственного распределения памяти
static Data* free;
static void allocate();
void* operator new(size_t);
void operator delete(void*,size_t);
};
Data *head, *tail;
DList() { head=tail=0; }
~DList() {
for (Data *ptr=head, *n; ptr; ptr=n) { // удаляем все элементы
n=ptr-"next;
delete ptr;
}
}
void push_back(int v) { // добавляем элемент
if (!head) head=tail=new Data(v);
else tail=tail-"next=new Data(v,tail);
}
};
Итак, все готово, и можно приступать к тестированию. Данные три теста я
попробовал на двух разных компиляторах, вот результат:
односвязныйодносвязный с собственным распределителем памятидвусвязный с
собственным распределителем памяти
f1()f2()f1()f2()f1()f2()
реализация 19.612.11.112.11.312.1
реализация 220.22.51.82.51.92.5
И что же мы здесь видим?
добавление собственного распределителя памяти существенно ускоряет программу
(в нашем примере, от 9 до 11 раз). Также замечу , что это приводит и к
существенной экономии памяти (как правило, для борьбы с внутренней
фрагментацией стандартные распределители памяти выделяют отрезки памяти,
кратные некоторому числу, например 16 байт, плюс память теряется на
поддерживающих список выделенных блоков структурах. Т.е. желательно выделять
память большими блоками и как можно реже.
в зависимости от качества компилятора/реализации STL, использование STL может
быть как почти приемлемым (замедление 30% для двусвязного и 40% для
односвязного списков), так и неприемлемым совершенно (замедление от 9 до 11
раз!)
двусвязный список является вполне приемлемой альтернативой односвязному
(замедление 5 — 20%)
Итак, наши измерения показывают, что бескомпромиссная эффективность STL является
мифом. Даже более того, если вы используете недостаточно хороший оптимизатор, то
использование STL вызовет существенные накладные расходы.
16.3.4. Конструкторы, стр. 505
То есть каждый из 10 000 элементов vr инициализируется конструктором Record(), а
каждый из s1 элементов контейнера vi инициализируется int().
Инициализация 10 000 элементов конструктором по умолчанию не может не впечатлять
— только в очень редком случае нужно именно это. Если вы выделяете эти 10 000
элементов про запас, для последующей перезаписи, то стоит подумать о следующей
альтернативе:
vector"X" vx; // объявляем пустой вектор
vx.resize(10000); // резервируем место воизбежание "дорогих"
// перераспределений в push_back()
// . . .
vx.push_back(x_work); // добавляем элементы по мере надобности
О ней тем более стоит подумать, т.к. даже в отличной реализации STL 3.2 от SGI
конструктор
vector"int" vi(s1);
подразумевает собой явный цикл заполнения нулями:
for (int i=0; i"s1; i++)
vi.elements[i]=0;
и требуется достаточно интеллектуальный оптимизатор для превращения этого цикла
в вызов memset()
memset(vi.elements, 0, sizeof(int)*s1);
что значительно улучшит производительность (конечно не программы вообще, а
только данного отрезка кода). Matt Austern поставлен в известность, и в будущих
версиях SGI STL можно ожидать повышения производительности данного конструктора.
16.3.5. Операции со стеком, стр. 508
Сноска: То есть память выделяется с некоторым запасом (обычно на десять
элементов). — Примеч. ред.
Очень жаль, что дорогая редакция сочла возможным поместить в книгу такую
глупость. Для приведения количества "дорогих" перераспределений к приемлемому
уровню O(log(N)), в STL используется увеличение объема зарезервированной памяти
в два раза, а при простом добавлении некоторого количества (10, например) мы,
очевидно, получим O(N), что есть плохо. Также отмечу, что для уменьшения
количества перераспределений стоит воспользоваться resize(), особенно, если вы
заранее можете оценить предполагаемую глубину стека.
17.4.1.2. Итераторы и пары, стр. 541
Также обеспечена функция, позволяющая удобным образом создавать pair.
Честно говоря, при первом знакомстве с шаблонами от всех этих многословных
объявлений начинает рябить в глазах, и не всегда понятно, что именно удобно в
такой вот функции:
template "class T1,class T2"
pair"T1,T2" std::make_pair(const T1& t1, const T2& t2)
{
return pair"T1,T2"(t1,t2);
}
А удобно следующее: Если нам нужен экземпляр класса-шаблона, то мы обязаны
предоставить все необходимые для инстанцирования класса параметры, т.к. на
основании аргументов конструктора они не выводятся. С функциями-шаблонами дела
обстоят получше:
char c=1;
int i=2;
// пробуем создать "пару"
pair(c,i); // неправильно — pair"char,int" не выводится
pair"char,int"(c,i); // правильно
make_pair(c,i); // правильно
17.4.1.5. Сравнения, стр. 543
Поэтому для константных ассоциативных массивов не существует версии
operator[](), стр. 543
Вообще говоря, существует, т.к. она объявлена в классе, но, ввиду ее
неконстантности, применена быть не может — при попытке инстанцирования вы
получите ошибку компиляции.
17.5.3.3. Другие операции, стр. 555
К сожалению, вызов явно квалифицированного шаблона члена требует довольно
сложного и редкого синтаксиса.
А вот тут мы имеем дело с откровенным "ляпом" в стандарте, увы! Во-первых,
приведенную фразу следует читать так: К сожалению, вызов явно квалифицированного
члена-шаблона всегда требует довольно сложного и редкого синтаксиса. Вот тут-то
и скрывается "ляп"; если я могу написать так:
f"int"(); // f — функция-шаблон
то почему это запрещено для функции-члена
obj.f"int"(); // ошибка: (obj.f)"(int) . . .
obj.template f"int"(); // правильно
Это нелогично, и действительно есть компиляторы (GNU C 2.95, например),
способные правильно разбираться с явной квалификацией параметров при вызове
члена-шаблона без использования "template". Д-р Страуструп пишет по этому
поводу:
"template" is not always needed. A compiler can hold lots of information that it
can use to disambiguate constructs. However, to ensure portability it is
important not to rely on information that is not used by all compilers and
tools.
Квалификатор template необходим не всегда. Компилятор может хранить достаточно
информации для разрешения неоднозначных конструкций, однако для гарантии
переносимости не стоит использовать информацию, которая не поддерживается всеми
компиляторами.
Остается надеяться, что в стандарт внесут соответствующие изменения.
17.6. Определение нового контейнера, стр. 556
... а потом применяйте поддерживаемый hash_map.
А вот еще один "ляп", и нет ему оправдания! Дело в том, что в стандарте понятия
"поддерживаемый hash_map" не существует. Еще больше пикантности данной ситуации
придает тот факт, что в самой STL, которая является основной частью стандартной
библиотеки С++, hash_map есть (и есть уже давно). Д-р Страуструп пишет по этому
поводу, что hash_map просто проглядели, а когда хватились, то было уже поздно --
никакие существенные изменения внести в стандарт уже было нельзя. Ну что ж,
бывает...
18.4.4.1. Связыватели, стр. 583
Читаемо? Эффективно?
Что же нам советуют признать читаемым и эффективным (впрочем, к эффективности
претензий действительно нет; теоретически).
list"int"::const_iterator p=find_if(c.begin(),c.end(),bind2nd(less"int"(),7));
Осмелюсь предложить другой вариант:
for (list"int"::const_iterator p=c.begin(); p!=c.end(); ++p)
if (*p"7) break;
Трудно ли это написать? По-видимому, нет. Является ли этот явный цикл менее
читаемым? По моему мнению, он даже превосходит читаемость примера с
использованием bind2nd(). А если нужно написать условие вида *p"=5 && *p"100,
что, в принципе, встречается не так уж и редко, то вариант с использованием
связывателей и find_if() проигрывает однозначно. Стоит добавить и чисто
психологический эффект — вызов красивой функции часто подсознательно
воспринимается атомарной операцией и не лишне подчеркнуть, что, порой, за
красивым фасадом скрывается крайне неэффективный последовательный поиск.
В целом, я агитирую против потери здравого смысла при использовании
предоставленного нам пестрого набора свистулек и колокольчиков. Увы, следует
признать, что для сколько-нибудь сложного применения они не предназначены, да и
на простом примере польза практически не видна.
18.4.4.2. Адаптеры функций-членов, стр. 584
Сначала рассмотрим типичный случай, когда мы хотим вызвать функцию-член без
аргументов...
Теперь немного про вызовы функций-членов для элементов контейнера с помощью
механизма mem_fun(). Действительно, вариант
for_each(lsp.begin(),lsp.end(),mem_fun(&Shape::draw)); // рисуем все фигуры
подкупает своим изяществом. И даже более того, предоставляемые mem_fun()
возможности действительно могут быть востребованы, например, при реализации
некоторого абстрактного шаблона разработки (design pattern). Но за красивым
фасадом скрывается вызов функции через указатель на член — операция отнюдь не
дешевая и далеко не все компиляторы умеют встраивать вызов функции через такой
указатель, будем рисковать?
А что, если нам нужно повернуть все фигуры на заданный угол? bind2nd(),
говорите? А если на разные углы да причем не все элементы контейнера, и эти углы
рассчитываются по сложному алгоритму? По-моему, такой вариант в реальных
программах встречается гораздо чаще.
Выходит, что и механизм mem_fun() не очень-то предназначен для серьезного
использования. Изучить его, конечно, стоит, а вот использовать или нет — решать
вам.
18.6. Алгоритмы, модифицирующие последовательность, стр. 592
Вместо вставки и удаления элементов такие алгоритмы изменяют значения
элементов...
Вот это да! Т.е. если я попытаюсь удалить элемент из списка с помощью такого
remove(), то вместо удаления элемента я получу перезаписывание (в среднем)
половины его элементов? Поймите меня правильно, среди приведенных в этом разделе
алгоритмов будут и практически полезные, но держать в стандартной библиотеке не
только неэффективные, но даже не соответствующие своему названию алгоритмы --
это уже слишком!
18.6.1. Копирование, стр. 592
Определения базовых операций копирования тривиальны...
Но в таком виде они будут совершенно неэффективны в приложении ко встроенным
типам, ведь общеизвестно, что для копирования больших объемов информации (если
без него действительно никак нельзя обойтись) следует использовать функции
стандартной библиотеки С memcpy() и memmove(). Вы нечасто используете векторы
встроенных типов? Осмелюсь заметить, что вектор указателей встречается не так уж
и редко и как раз подходит под это определение. К счастью, у меня есть хорошая
новость: в качественной реализации STL (например от SGI) вызов операции
копирования для vector"int" как раз и приведет к эффективному memmove().
Выбор подходящего алгоритма производится на этапе компиляции с помощью
специально определенного шаблона __type_traits"" — свойства типа. Который (по
умолчанию) имеет безопасные настройки для сложных типов с нетривиальными
конструкторами/деструкторами и оптимизированные специализации для POD типов,
которые можно копировать простым перемещением блоков памяти.
В С++ вы часто будете встречать аббревиатуру POD (Plain Old Data). Что же она
обозначает? POD тип — это тип, объекты которого можно безопасно перемещать в
памяти (с помощью memmove(), например). Данному условию очевидно удовлетворяют
встроенные типы (в том числе и указатели) и классы без определяемой
пользователем операции присваивания и деструктора.
Почему я об этом говорю? Потому что, например, очевидное определение класса Date
является POD типом:
class Date {
int day, mon, year;
// или даже
long val; // yyyymmdd
public:
// ...
};
Поэтому стоит разрешить оптимизацию предоставив соответствующую специализацию
__type_traits"":
template"" struct __type_traits"Date" {
// ...
};
Только обратите внимание: __type_traits"" — не часть стандартной библиотеки,
разные реализации могут использовать различные имена или даже не производить
оптимизацию вообще. Изучите то, что есть у вас.
19.2.5. Обратные итераторы, стр. 622
Это приводит к тому, что * возвращает значение *(current-1) ...
Да, по смыслу именно так:
24.4.1.3.3 - operator* [lib.reverse.iter.op.star]
reference operator*() const;
-1- Effects:
Iterator tmp = current;
return *--tmp;
Т.е. каждый раз, когда вы применяете разыменование обратного итератора,
происходит создание временного итератора, его декремент и разыменование. Не
многовато ли, для такой простой и часто используемой (как правило, в цикле для
каждого элемента) операции? Д-р Страуструп пишет по этому поводу следующее:
I don't think anyone would use a reverse iterator if an iterator was an
alternative, but then you never know what people might know. When you actually
need to go through a sequence in reverse order a reverse iterator is often quite
efficient compared to alternatives. Finally, there may not be any overhead
because where the iterator is a vector the temporary isn't hard to optimize into
a register use. One should measure before worrying too much about overhead.
Я не думаю, что кто-то будет использовать обратный итератор, когда можно
использовать обычный, но мы никогда не можем знать, что думают другие люди.
Когда вам действительно нужно пройти последовательноть в обратном порядке,
обратный итератор является вполне приемлемой альтернативой. В принципе, иногда
можно избежать накладных расходов вообще, например в случае обратного прохода по
вектору (когда итератором является просто T*). В любом случае, прежде чем
беспокоиться о производительности, следует произвести реальные измерения.
Вместе с тем, обратный итератор все-таки несет в себе ненужные накладные
расходы, и для обратного прохода по последовательности лучше использовать
обычный итератор с явным (пре)декрементом.
И раз уж речь зашла о реальных измерениях, давайте их произведем.
#include "stdio.h"
#include "stdlib.h"
#include "time.h"
#include "list"
long Count, Var;
std::list"int" lst;
void f1()
{
typedef std::list"int"::reverse_iterator RI;
for (RI p=lst.rbegin(); p!=lst.rend(); ++p)
Var+=*p;
}
void f2()
{
std::list"int"::iterator p=lst.end();
if (p!=lst.begin())
do {
--p;
Var+=*p;
} while (p!=lst.begin());
}
int main(int argc,char** argv)
{
if (argc"1) Count=atol(argv[1]);
for (int i=0; i"10000; i++)
lst.push_back(i);
clock_t c1,c2;
{
c1=clock();
for (long i=0; i"Count; i++)
for (long j=0; j"1000; j++)
f1();
c2=clock();
printf("f1(): %ld ths calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
}
{
c1=clock();
for (long i=0; i"Count; i++)
for (long j=0; j"1000; j++)
f2();
c2=clock();
printf("f2(): %ld ths calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
}
}
В данном примере список из 10 000 элементов проходится несколько тысяч раз
(задается параметром) с использованием обратного (в f1()) и обычного (в f2())
итераторов. При использовании качественного оптимизатора разницы времени
выполнения замечено не было, а для "обычных" реализаций она составила от 45% до
2.4 раза.
И еще одна проблема: приводит ли постинкремент итератора к существенным
накладным расходам по сравнению с преинкрементом? Давайте внесем соответствующие
изменения:
void f1()
{
typedef std::list"int"::iterator I;
for (I p=lst.begin(); p!=lst.end(); ++p)
Var+=*p;
}
void f2()
{
typedef std::list"int"::iterator I;
for (I p=lst.begin(); p!=lst.end(); p++)
Var+=*p;
}
И опять все тот же результат: разницы может не быть, а там, где она проявлялась,
ее величина находилась в пределах 5 — 30 процентов.
В целом, не стоит использовать потенциально более дорогие обратные итераторы и
постинкременты, если вы не убедились в интеллектуальности используемого
оптимизатора.
19.4.2. Распределители памяти, определяемые пользователем, стр. 637
template"class T"
T* Pool_alloc"T"::allocate(size_type n, void* =0)
{
if (n==1) return static_cast"T*"(mem_alloc());
// ...
}
Как всегда, самое интересное скрывается за многоточием. Как же нам реализовать
часть allocate""() для n!=1? Простым вызовом в цикле mem_alloc()? Увы, в данном
случае очевидное решение не подходит совершенно. Почему? Давайте рассмотрим
поведение Pool_alloc"char". Глядя на конструктор оригинального Pool:
Pool::Pool(unsigned int sz)
: esize(sz"sizeof(Link*) ? sizeof(Link*) : sz)
{
// ...
}
можно заметить, что для sz==sizeof(char) для каждого char мы будем выделять
sizeof(Link*) байт памяти. Для "обычной" реализации это означает четырехкратный
перерасход памяти! Т.о. выделение памяти для массивов объектов типа X, где
sizeof(X)"sizeof(Link*) становится нетривиальной задачей, равно как и
последующее их освобождение в deallocate""(), фактически, придется принципиально
изменить алгоритм работы аллокатора.
19.4.4. Неинициализированная память, стр. 641
template"class T, class A" T* temporary_dup(vector"T,A"& v)
{
T* p=get_temporary_buffer"T"(v.size()).first;
if (p==0) return 0;
copy(v.begin(),v.end(),raw_storage_iterator"T*,T"(p));
return p;
}
Вообще говоря, приведенная функция написана некорректно, т.к. не проверяется
второй элемент возвращаемой get_temporary_buffer""() пары. Т.к.
get_temporary_buffer""() может вернуть меньше памяти, чем мы запросили, то
необходима другая проверка:
template"class T, class A" T* temporary_dup(vector"T,A"& v)
{
pair"T*,ptrdiff_t" p(get_temporary_buffer"T"(v.size()));
if (p.second"v.size()) {
if (p.first) return_temporary_buffer(p.first);
return 0;
}
copy(v.begin(),v.end(),raw_storage_iterator"T*,T"(p));
return p;
}
20.2.1. Особенности символов, стр. 647
Вызов assign(s,n,x) при помощи assign(s[i],x) присваивает n копий x строке s.
Функция compare() использует для сравнения символов lt() и eq().
К счастью, для обычных символов char_traits"char" это не так, в том смысле, что
не происходит вызов в цикле lt(), eq(), assign(s[i],x), а используются
специально для этого предназначенные memcmp() и memset(), что, впрочем, не
влияет на конечный результат. Т.е. используя strcmp() мы ничего не выигрываем,
даже более того, в специально проведенных мной измерениях производительности,
сравнения string оказались на 30% быстрее, чем принятое в С сравнение char* с
помощью strcmp(). Что и не удивительно: для string размеры сравниваемых массивов
char известны заранее.
20.3.4. Конструкторы, стр. 652
Реализация basic_string хранит длину строки, не полагаясь на завершающий символ
(ноль).
Вместе с тем, хорошо оптимизированные реализации хранят строку вместе с
завершающим нулем, дабы максимально ускорить функцию basic_string::c_str(). Не
секрет, что большинство используемых функций (традиционно) принимают строку в
виде [const] char* вместо эквивалентного по смыслу [const] string&, исходя из
того простого факта, что мы не можем ускорить "безопасную" реализацию, но можем
скрыть эффективную за безопасным интерфейсом.
К слову сказать, исходя из моего опыта, слухи об опасности манипулирования
простыми char* в стиле С оказываются сильно преувеличенными. Да, вы должны
следить за всеми мелочами, но, например, ни у кого не возникает протеста по
поводу того, что если в формуле корней квадратного уравнения мы вместо ---
напишем -+', то результат будет неверен.
Резюмируя данный абзац, хочу сказать, что string использовать можно и нужно, но
если логика работы вашей программы интенсивно использует манипуляции со
строками, стоит подумать о разработке собственных средств, основанных на
функциях типа memcpy(), а в "узких" местах без этого просто не обойтись.
20.3.6. Присваивание, стр. 655
Это делает использование строк, которые только считываются и задаются в качестве
аргумента, гораздо более дешевым, чем кто-то мог по наивности предположить.
Однако было бы так же наивно со стороны программистов не проверять имеющиеся у
них реализации перед написанием кода, который полагается на оптимизацию
копирования строк.
Я бы попросил вас серьезно отнестись к данному совету (т.е. к проверке имеющейся
реализации). Например, SGI STL 3.2 всегда копирует символы строки, не полагаясь
на основанную на подсчете ссылок версию. Авторы библиотеки объясняют это тем,
что использующие модель подсчета ссылок строки не подходят для многопоточных
приложений, ими утверждается, что использующие данную реализацию строк
многопоточные приложения аварийно завершают свою работу один раз в несколько
месяцев и именно из-за строк. Мне, честно говоря, данное утверждение кажется
несерьезным, но факт остается фактом — существуют отлично оптимизированные
реализации стандартной библиотеки, которые по тем или иным причинам отказались
от использования основанных на подсчете ссылок строк.
Резюмируя данный материал хочу отметить, что я всегда, где это возможно,
стараюсь избегать копирования строк, например путем передачи const string&.
21.2.2. Вывод встроенных типов, стр. 676
... будет интерпретировано так:
(cerr.operator""("x=")).operator""(x);
Конечно же на самом деле все не так: в новых потоках ввода-вывода оператор
вывода строки больше не является функцией-членом, следовательно оно будет
интерпретировано так:
operator""(cerr,"x=").operator""(x);
Товарищи программисты! Еще раз повторю: никогда не копируйте блоками старый
текст, а если это все-таки необходимо, обязательно проверяйте каждую загогулину.
Вот гражданин Страуструп забыл проверить, и, в результате, новый релиз его
монографии содержит очевидную ошибку.
21.3.4. Ввод символов, стр
...Закладка в соц.сетях