2* b2ptr=&d;
printf("b2ptr\t%p\n", b2ptr);
void* ptr2=b2ptr->vfun();
printf("ptr2\t%p\n", ptr2);
}
Обратите внимание: в данном примере я воспользовался "некоторыми ослаблениями" для типа возвращаемого значения D::vfun()
, и вот к чему это привело:
dptr 0012FF6C D::vfun(): this=0012FF6C ptr1 0012FF6C b2ptr 0012FF70 D::vfun(): this=0012FF6C ptr2 0012FF70Т.о. оба раза была вызвана
D::vfun()
, но возвращаемое ей значение зависит от способа вызова (ptr1!=ptr2
), как это, собственно говоря, и должно быть.
Делается это точно так же, как уже было описано в разделе 361 "12.2.6. Виртуальные функции", только помимо корректировки принимаемого значения this
необходимо дополнительно произвести корректировку this
возвращаемого. Понятно, что виртуальные функции с ковариантным типом возврата встречаются настолько редко, что реализация их вызова посредством расширения vtbl
вряд ли может быть признана адекватной. На практике обычно создаются специальные функции-заглушки, чьи адреса помещаются в соответствующие элементы vtbl
:
// псевдокод // оригинальная D::vfun, написанная программистом D* D::vfun(D *const this) { // ... } // сгенерированная компилятором функция-заглушка для вызова D::vfun() через // указатель на базовый класс B2 B2* D::vfun_stub(B2 *const this) { return D::vfun(this+delta_1)+delta_2; }где возвращаемый функцией указатель корректируется посредством константы
delta_2
, вообще говоря, не равной delta_1
.
Подводя итог, хочется отметить, что в общем случае вызов виртуальной функции становится все меньше похож на "просто косвенный вызов функции". Ну, и раз уж речь зашла о виртуальных функциях с ковариантным типом возврата, стоит привести соответствующую часть стандарта:
10.3. Виртуальные функции [class.virtual]
D::f
замещает функцию B::f
, типы возвращаемых ими значений будут ковариантными, если они удовлетворяют следующим условиям:
B::f
идентичен классу в возвращаемом значении D::f
или он является однозначно определенным открытым прямым или косвенным базовым классом возвращаемого D::f
класса и при этом доступен в D
D::f
имеет те же или меньшие cv-квалификаторы, что и класс в возвращаемом значении B::f
.
D::f
отличается от типа возвращаемого значения B::f
, то тип класса в возвращаемом значении D::f
должен быть завершен в точке определения D::f
или он должен быть типом D
. Когда замещающая функция будет вызывана (как последняя заместившая функция), тип ее возвращаемого значения будет (статически) преобразован в тип возвращаемого значения замещаемой функции (5.2.2). Например:
class B {}; class D : private B { friend class Derived; }; struct Base { virtual void vf1(); virtual void vf2(); virtual void vf3(); virtual B* vf4(); virtual B* vf5(); void f(); }; struct No_good : public Base { D* vf4(); // ошибка: B (базовый класс D) недоступен }; class A; struct Derived : public Base { void vf1(); // виртуальная и замещает Base::vf1() void vf2(int); // не виртуальная, скрывает Base::vf2() char vf3(); // ошибка: неправильный тип возвращаемого значения 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(); // ошибка: отсутствует аргумент }
3.9.3. CV-квалификаторы [basic.type.qualifier]
нет cv-квалификатора | < | const |
нет cv-квалификатора | < | volatile |
нет cv-квалификатора | < | const volatile |
const |
< | const volatile |
volatile |
< | const volatile |
Вместе с тем, не стоит думать, что 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() { typedef std::list<int> list_type; list_type lst; for (int i=0; i<1000; i++) lst.push_back(i); for (list_type::const_iterator ci=lst.begin(), cend=lst.end(); ci!=cend; ++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 { // ... // для собственного распределения памяти 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; }Обратите внимание, что в данном примере наш распределитель памяти не возвращает полученную память системе. Но это не memory leak (утечка памяти) -- это memory pool, т.е. заранее выделенный запас памяти для быстрого последующего использования. На первый взгляд, разница между memory leak и memory pool может показаться слишком тонкой, но она есть: дело в том, что в первом случае потребление памяти не ограничено, вплоть до полного ее исчерпания, а во втором оно никогда не превысит реально затребованного программой объема плюс некоторая дельта, не превосходящая размер выделяемого блока.
И еще, наш распределитель содержит очень серьезную ошибку -- он неправильно обрабатывает удаление нуля (NULL
-указателя). В нашем примере это не имеет значения, но в реальном коде вы обязаны это учесть, т.е.:
inline void List::Data::operator delete(void* dl, size_t) { if (!dl) return; // игнорируем NULL // добавляем в начало списка свободных элементов 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() | |
реализация 1 | 9.6 | 12.1 | 1.1 | 12.1 | 1.3 | 12.1 |
реализация 2 | 20.2 | 2.5 | 1.8 | 2.5 | 1.9 | 2.5 |
И что же мы здесь видим?
vr
инициализируется конструктором Record()
, а каждый из s1
элементов контейнера vi
инициализируется int()
.
Инициализация 10 000 элементов конструктором по умолчанию не может не впечатлять -- только в очень редком случае нужно именно это. Если вы выделяете эти 10 000 элементов про запас, для последующей перезаписи, то стоит подумать о следующей альтернативе:
vector<X> vx; // объявляем пустой вектор vx.reserve(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 можно ожидать повышения производительности данного конструктора.
Очень жаль, что дорогая редакция сочла возможным поместить в книгу такую глупость. Для приведения количества "дорогих" перераспределений к приемлемому уровню O(log(N)), в STL используется увеличение объема зарезервированной памяти в полтора-два раза, а при простом добавлении некоторого количества (10, например) мы, очевидно, получим O(N), что есть плохо. Также отмечу, что для уменьшения количества перераспределений стоит воспользоваться reserve()
, особенно, если вы заранее можете оценить предполагаемую глубину стека.
И дело не только в определении операции "меньше", а еще и в том, что char*
не стоит использовать в качестве элементов STL контейнеров вообще: контейнер будет содержать значение указателя -- не содержимое строки, как кто-то по наивности мог полагать. Например, следующая функция содержит серьезную ошибку:
void f(set<char*>& cset) { for (;;) { char word[100]; // считываем слово в word ... cset.insert(word); // ошибка: вставляем один и тот же указатель // на локальную переменную } }Для получения ожидаемого результата следует использовать
string
:
void f(set<string>& cset) { for (;;) { char word[100]; // считываем слово в word ... cset.insert(word); // OK: вставляем string } }Использование
char*
в STL контейнерах приводит к чрезвычайно коварным ошибкам, т.к. иногда все работает правильно. Например документация к sgi STL широко использует char*
в своих учебных примерах:
struct ltstr { bool operator()(const char* s1, const char* s2) const { return strcmp(s1, s2) < 0; } }; int main() { const int N = 6; const char* a[N] = {"isomer", "ephemeral", "prosaic", "nugatory", "artichoke", "serif"}; set<const char*, ltstr> A(a, a + N); // и т.д. }Данный пример вполне корректен, но стоит только вместо статически размещенных строковых литералов использовать локально формируемые C-строки, как неприятности не заставят себя ждать.
Относитесь скептически к учебным примерам!
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); // правильно
operator[]()
.
Вообще говоря, существует, т.к. она объявлена в классе, но, ввиду ее неконстантности, применена быть не может -- при попытке инстанциирования вы получите ошибку компиляции.
К счастью, это не так: в данном случае этот "довольно сложный и редкий синтаксис" не требуется.
В самом деле, если разрешено
f<int>(); // f -- функция-шаблонто почему вдруг компилятор не может правильно разобраться с
obj.f<int>(); // f -- функция-шаблон, член классаМожет, и разбирается!
Исторически, непонимание возникло из-за того, что:
template
был изобретен комитетом по стандартизации, а не д-ром Страуструпом;
template
как квалификатор.
hash_map
.
А вот еще один "ляп", и нет ему оправдания! Дело в том, что в стандарте понятия "поддерживаемый hash_map
" не существует. Еще больше пикантности данной ситуации придает тот факт, что в самой STL, которая является основной частью стандартной библиотеки C++, hash_map
есть (и есть уже давно). Д-р Страуструп пишет по этому поводу, что hash_map
просто проглядели, а когда хватились, то было уже поздно -- никакие существенные изменения внести в стандарт было уже нельзя. Ну что ж, бывает...
Что же нам советуют признать читаемым и эффективным (впрочем, к эффективности, теоретически, претензий действительно нет)?
list<int>::const_iterator p=find_if(c.begin(),c.end(),bind2nd(less<int>(),7));Осмелюсь предложить другой вариант:
list<int>::const_iterator p; for (p=c.begin(); p!=c.end(); ++p) if (*p<7) break;Трудно ли это написать? По-видимому, нет. Является ли этот явный цикл менее читаемым? По моему мнению, он даже превосходит читаемость примера с использованием
bind2nd()
. А если нужно написать условие вида *p>=5 && *p<100
, что, в принципе, встречается не так уж и редко, то вариант с использованием связывателей и find_if()
проигрывает однозначно. Стоит добавить и чисто психологический эффект: вызов красивой функции часто подсознательно воспринимается атомарной операцией и не лишне подчеркнуть, что за красивым фасадом порой скрывается крайне неэффективный последовательный поиск.
В целом, я агитирую против потери здравого смысла при использовании предоставленного нам пестрого набора свистулек и колокольчиков. Увы, следует признать, что для сколь-нибудь сложного применения они не предназначены, да и на простом примере польза практически не видна.
Теперь немного про вызовы функций-членов для элементов контейнера с помощью механизма mem_fun()
. Действительно, вариант
for_each(lsp.begin(),lsp.end(),mem_fun(&Shape::draw)); // рисуем все фигурыподкупает своим изяществом. И даже более того, предоставляемые
mem_fun()
возможности действительно могут быть востребованы, например, при реализации некоторого абстрактного шаблона разработки (design pattern). Но за красивым фасадом скрывается вызов функции через указатель на член -- операция отнюдь не дешевая и далеко не все компиляторы умеют встраивать вызов функции через такой указатель. Будем рисковать?
А что, если нам нужно повернуть все фигуры на заданный угол? bind2nd()
, говорите? А если на разные углы да причем не все элементы контейнера, и эти углы рассчитываются по сложному алгоритму? По-моему, такой вариант в реальных программах встречается гораздо чаще.
Выходит, что и механизм mem_fun()
не очень-то предназначен для серьезного использования. Изучить его, конечно, стоит, а вот использовать или нет -- решать вам.
Вот это да! Т.е. если я попытаюсь удалить элемент из списка с помощью такого remove()
, то вместо удаления элемента я получу просто переприсваивание (в среднем) половины его элементов?!
Поймите меня правильно, среди приведенных в этом разделе алгоритмов будут и практически полезные, но держать в стандартной библиотеке не только неэффективные, но даже не соответствующие своему названию алгоритмы -- это уже слишком!
Но в таком виде они будут совершенно неэффективны в приложении ко встроенным типам, ведь общеизвестно, что для копирования больших объемов информации (если без него действительно никак нельзя обойтись) следует использовать функции стандартной библиотеки C memcpy()
и memmove()
. Вы нечасто используете векторы встроенных типов? Осмелюсь заметить, что вектор указателей встречается не так уж и редко и как раз подходит под это определение. К счастью, у меня есть хорошая новость: в качественной реализации STL (например от sgi) вызов операции копирования для vector<int>
как раз и приведет к эффективному memmove()
.
Выбор подходящего алгоритма производится на этапе компиляции с помощью специально определенного шаблона __type_traits<>
-- свойства типа. Который (по умолчанию) имеет безопасные настройки для сложных типов с нетривиальными конструкторами/деструкторами и оптимизированные специализации для POD типов, которые можно копировать простым перемещением блоков памяти.
В C++ вы часто будете встречать аббревиатуру 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<>
-- не часть стандартной библиотеки, разные реализации могут использовать различные имена или даже не производить оптимизацию вообще. Изучите то, что есть у вас.
*
возвращает значение *(current-1)
...
Да, по смыслу именно так:
24.4.1.3.3 operator*
[lib.reverse.iter.op.star]
reference operator*() const;
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.Я не думаю, что бы кто-то использовал обратный итератор там, где можно использовать обычный, но мы никогда не можем знать, что думают другие люди. Когда вам действительно нужно пройти последовательность в обратном порядке, обратный итератор является вполне приемлемой альтернативой. В принципе, иногда можно вообще избежать накладных расходов, например в случае обратного прохода по вектору, когда временная переменная-итератор без труда размещается в регистре. В любом случае, не стоит чрезмерно беспокоиться о производительности не проведя реальных измерений.
Вместе с тем, обратный итератор все-таки несет в себе ненужные накладные расходы, и для обратного прохода по последовательности лучше использовать обычный итератор с явным (пре)декрементом.
И раз уж речь зашла о реальных измерениях, давайте их произведем.
#include <stdio.h> #include <stdlib.h> #include <time.h> #include <list> long Count, Var; typedef std::list<int> list_type; list_type lst; void f1() { for (list_type::reverse_iterator ri=lst.rbegin(), rend=lst.rend(); ri!=rend; ++ri) Var+=*ri; } void f2() { list_type::iterator i=lst.end(), beg=lst.begin(); if (i!=beg) { do { --i; Var+=*i; } while (i!=beg); } } 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() { for (list_type::iterator i=lst.begin(), end=lst.end(); i!=end; ++i) Var+=*i; } void f2() { for (list_type::iterator i=lst.begin(), end=lst.end(); i!=end; i++) Var+=*i; }И опять все тот же результат: разницы может не быть, а там, где она проявлялась, ее величина находилась в пределах 5 - 30 процентов.
В целом, не стоит использовать потенциально более дорогие обратные итераторы и постинкременты, если вы не убедились в интеллектуальности используемого оптимизатора.
Вполне резонным будет вопрос: что же здесь имелось ввиду? Недостаток каких свойств мешает ссылкам C++ быть "совершенными"? Д-р. Страуструп ответил следующее:
Something that would allow a copy constructor to be defined using a user-defined reference object.Что-то, что позволило бы определить конструктор копирования с использованием предоставленного пользователем ссылочного типа.
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<>()
, фактически, придется принципиально изменить алгоритм работы аллокатора.
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.first; }
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% быстрее, чем принятое в C сравнение char*
с помощью strcmp()
. Что и не удивительно: для string
размеры сравниваемых массивов char
известны заранее.
basic_string
хранит длину строки, не полагаясь на завершающий символ (ноль).
Вместе с тем, хорошо оптимизированные реализации хранят строку вместе с завершающим нулем, дабы максимально ускорить функцию basic_string::c_str()
. Не секрет, что большинство используемых функций (традиционно) принимают строку в виде [const
] char*
вместо эквивалентного по смыслу [const
] string&
, исходя из того простого факта, что мы не можем ускорить "безопасную" реализацию, но можем скрыть эффективную за безопасным интерфейсом.
К слову сказать, мой личный опыт свидетельствует о том, что слухи об опасности манипулирования простыми char*
в стиле C оказываются сильно преувеличенными. Да, вы должны следить за всеми мелочами, но, например, ни у кого не возникает протеста по поводу того, что если в формуле корней квадратного уравнения мы вместо '-
' напишем '+
', то результат будет неверен.
Резюмируя данный абзац, хочу сказать, что string
использовать можно и нужно, но если логика работы вашей программы интенсивно использует манипуляции со строками, стоит подумать о разработке собственных средств, основанных на функциях типа memcpy()
, а в "узких" местах без этого просто не обойтись.
Я бы попросил вас серьезно отнестись к данному совету (т.е. к проверке имеющейся реализации). Например, sgi STL 3.2 всегда копирует символы строки, не полагаясь на основанную на подсчете ссылок версию. Авторы библиотеки объясняют это тем, что использующие модель подсчета ссылок строки не подходят для многопоточных приложений.
Ими утверждается, что использующие данную реализацию строк многопоточные приложения аварийно завершают свою работу один раз в несколько месяцев и именно из-за строк. В принципе, модель подсчета ссылок действительно плохо подходит для многопоточных приложений, т.к. ее использование приводит к существенным накладным расходам (более подробно об этом можно почитать у Herb Sutter Reference Counting - Part III), но вот собственно аварийное завершение работы может быть вызвано только ошибками в реализации -- чудес не бывает.
Как бы то ни было, но факт остается фактом: существуют отлично оптимизированные реализации стандартной библиотеки, которые, по тем или иным причинам, отказались от использования основанных на подсчете ссылок строк.
Резюмируя данный материал хочу отметить, что я всегда, где это возможно, стараюсь избегать копирования строк, например путем передачи const string&
.
(cerr.operator<<("x=")).operator<<(x);
Конечно же на самом деле все не так: в новых потоках ввода-вывода оператор вывода строки больше не является функцией-членом, следовательно оно будет интерпретировано так:
operator<<(cerr,"x=").operator<<(x);Товарищи программисты! Еще раз повторю: никогда не копируйте блоками старый текст, а если это все-таки необходимо, -- обязательно проверяйте каждую загогулину!
Вот гражданин Страуструп забыл проверить, и, в результате, новый релиз его монографии содержит очевидную ошибку.
Вынужден вас огорчить: определенные стандартом потоки C++ заявленным свойством не обладают. Они всегда работают медленнее C, а в некоторых реализациях -- медленно до смешного (правда, объективности ради стоит отметить, что мне попадались и совершенно отвратительно реализованные FILE*
потоки C, в результате чего C++ код вырывался вперед; но это просто недоразумение, если не сказать крепче!). Рассмотрим следующую программу:
#include <stdio.h> #include <time.h> #include <io.h> // для open() #include <fcntl.h> #include <iostream> #include <fstream> using namespace std; void workc(char*); void workcpp(char*); void work3(char*); int main(int argc, char **argv) { if (argc==3) switch (*argv[2]-'0') { case 1: { workc(argv[1]); break; } case 2: { workcpp(argv[1]); break; } case 3: { work3(argv[1]); break; } } } void workc(char* fn) { FILE* fil=fopen(fn, "rb"); if (!fil) return; time_t t1; time(&t1); long count=0; while (getc(fil)!=EOF) count++; time_t t2; time(&t2); fclose(fil); cout<<count<<" bytes per "<<t2-t1<<" sec.\n" ; } void workcpp(char* fn) { ifstream fil(fn, ios_base::in|ios_base::binary); if (!fil) return; time_t t1; time(&t1); long count=0; while (fil.get()!=EOF) count++; time_t t2; time(&t2); cout<<count<<" bytes per "<<t2-t1<<" sec.\n" ; } class File { int fd; // дескриптор файла unsigned char buf[BUFSIZ]; // буфер стандартного размера unsigned char* gptr; // следующий читаемый символ unsigned char* bend; // конец данных int uflow(); public: File(char* fn) : gptr(0), bend(0) { fd=open(fn, O_RDONLY|O_BINARY); } ~File() { if (Ok()) close(fd); } int Ok() { return fd!=-1; } int gchar() { return (gptr<bend) ? *gptr++ : uflow(); } }; int File::uflow() { if (!Ok()) return EOF; int rd=read(fd, buf, BUFSIZ); if (rd<=0) { // ошибка или EOF close(fd); fd=-1; return EOF; } gptr=buf; bend=buf+rd; return *gptr++; } void work3(char* fn) { File fil(fn); if (!fil.Ok()) return; time_t t1; time(&t1); long count=0; while (fil.gchar()!=EOF) count++; time_t t2; time(&t2); cout<<count<<" bytes per "<<t2-t1<<" sec.\n" ; }Ее нужно запускать с дву