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 можно ожидать повышения производительности данного конструктора.
Стр.508: 16.3.5. Операции со стеком
Сноска: То есть память выделяется с некоторым запасом (обычно на десять элементов). -- Примеч. ред.
Очень жаль, что дорогая редакция сочла возможным поместить в книгу такую глупость. Для приведения количества "дорогих" перераспределений к приемлемому уровню O(log(N)), в STL используется увеличение объема зарезервированной памяти в полтора-два раза, а при простом добавлении некоторого количества (10, например) мы, очевидно, получим O(N), что есть плохо. Также отмечу, что для уменьшения количества перераспределений стоит воспользоваться reserve()
, особенно, если вы заранее можете оценить предполагаемую глубину стека.
Стр.526: 17.1.4.1. Сравнения
Таким образом, при использовании в качестве ключей C-строк ассоциативные контейнеры будут работать не так, как ожидало бы большинство людей.
И дело не только в определении операции "меньше", а еще и в том, что 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-строки, как неприятности не заставят себя ждать.
Относитесь скептически к учебным примерам!
Стр.541: 17.4.1.2. Итераторы и пары
Также обеспечена функция, позволяющая удобным образом создавать
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); // правильно
Стр.543: 17.4.1.3. Индексация
Поэтому для константных ассоциативных массивов не существует версии
operator[]()
.
Вообще говоря, существует, т.к. она объявлена в классе, но, ввиду ее неконстантности, применена быть не может -- при попытке инстанциирования вы получите ошибку компиляции.
Стр.555: 17.5.3.3. Другие операции
К сожалению, вызов явно квалифицированного шаблона члена требует довольно сложного и редкого синтаксиса.
К счастью, это не так: в данном случае этот "довольно сложный и редкий синтаксис" не требуется.
В самом деле, если разрешено
f<int>(); // f -- функция-шаблон
то почему вдруг компилятор не может правильно разобраться с
obj.f<int>(); // f -- функция-шаблон, член класса
Может, и разбирается!
Исторически, непонимание возникло из-за того, что:
непосредственно этот туманный аспект использования квалификатораtemplate
был изобретен комитетом по стандартизации, а не д-ром Страуструпом;
первым компилятором, поддерживающим экспериментальные (на тот момент) нововведения, был aC++ от HP. Данный компилятор ошибочно требовал наличия квалификатора, что, вкупе с неочевидным текстом стандарта, не могло не ввести в заблуждение.
Дальнейшее развитие темы "сложного и редкого синтаксиса" можно найти в разделе B.13.6.
template
как квалификатор.Reference Counting - Part III), но вот собственно аварийное завершение работы может быть вызвано только ошибками в реализации -- чудес не бывает.
Как бы то ни было, но факт остается фактом: существуют отлично оптимизированные реализации стандартной библиотеки, которые, по тем или иным причинам, отказались от использования основанных на подсчете ссылок строк.
Резюмируя данный материал хочу отметить, что я всегда, где это возможно, стараюсь избегать копирования строк, например путем передачи const string&
.
Стр.676: 21.2.2. Вывод встроенных типов
... будет интерпретировано так:
(cerr.operator<<("x=")).operator<<(x);
Конечно же на самом деле все не так: в новых потоках ввода-вывода оператор вывода строки больше не является функцией-членом, следовательно оно будет интерпретировано так:
operator<<(cerr,"x=").operator<<(x);
Товарищи программисты! Еще раз повторю: никогда не копируйте блоками старый текст, а если это все-таки необходимо, -- обязательно проверяйте каждую загогулину!
Вот гражданин Страуструп забыл проверить, и, в результате, новый релиз его монографии содержит очевидную ошибку.
Стр.687: 21.3.4. Ввод символов
Как уже было сказано, главная сила языка C -- в его способности считывать символы и решать, что с ними ничего не надо делать -- причем выполнять это быстро. Это действительно важное достоинство, которое нельзя недооценивать, и цель C++ -- не утратить его.
Вынужден вас огорчить: определенные стандартом потоки 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" ;
}
Ее нужно запускать с двумя параметрами. Первый параметр -- это имя (большого) файла для чтения, а второй -- цифра 1, 2 или 3, выбирающая функцию
workc()
, workcpp()
или work3()
соответственно. Только не забудьте про дисковый кэш, т.е. для получения объективных результатов программу нужно запустить несколько раз для каждого из вариантов.
Необычным местом здесь является функция work3()
и соответствующий ей класс File
. Они написаны специально для проверки "честности" реализации стандартных средств ввода-вывода C -- FILE*
. Если вдруг окажется, что workc()
работает существенно медленнее work3()
, то вы имеете полное право назвать создателей такой библиотеки, как минимум, полными неучами.
А сейчас попробуем получить информацию к размышлению: проведем серию контрольных запусков и посмотрим на результат.
И что же нам говорят безжалостные цифры? Разница в разы! А для одного широко распространенного коммерческого пакета (не будем показывать пальцем) она порой достигала 11 раз!!!
Стоит только взглянуть на определения вызываемых функций, как ответ сразу станет очевидным.
Для C с его getc()
в типичной реализации мы имеем:
#define getc(f) ((--((f)->level) >= 0) ? (unsigned char)(*(f)->curp++) : _fgetc (f))
Т.е. коротенький макрос вместо функции. Как говорится -- всего-ничего. А вот для C++ стандарт требует столько, что очередной раз задаешься вопросом: думали ли господа-комитетчики о том, что горькие плоды их творчества кому-то реально придется применять?!
Ну и ладно: предупрежден -- вооружен! А что, если задать буфер побольше?
void workc(char* fn)
{
// ...
if (setvbuf(fil, 0, _IOFBF, LARGE_BUFSIZ)) return;
// ...
}
void workcpp(char* fn)
{
// ...
char* buf=new char[LARGE_BUFSIZ];
fil.rdbuf()->pubsetbuf(buf, LARGE_BUFSIZ);
// ...
delete [] buf;
}
Как ни странно, по сути ничего не изменится! Дело в том, что современные ОС при работе с диском используют очень качественные алгоритмы кэширования, так что еще один уровень буферизации внутри приложения оказывается излишним (в том смысле, что используемые по умолчанию буферы потоков вполне адекватны).
Кстати, одним из хороших примеров необходимости использования многопоточных программ является возможность ускорения работы программ копирования файлов, когда исходный файл и копия расположены на разных устройствах. В этом случае программа запускает несколько потоков, осуществляющих асинхронные чтение и запись. Но в современных ОС в этом нет никакого смысла, т.к. предоставляемое системой кэширование кроме всего прочего обеспечивает и прозрачное для прикладных программ асинхронное чтение и запись.
Подводя итог, хочется отметить, что если ввод-вывод является узким местом вашего приложения, то следует воздержаться от использования стандартных потоков C++ и использовать проверенные десятилетиями методы.
Стр.701: 21.4.6.3. Манипуляторы, определяемые пользователем
Коль скоро с эффективностью потоков ввода-вывода мы уже разобрались, следует поговорить об удобстве. К сожалению, для сколько-нибудь сложного форматирования предоставляемые потоками средства не предназначены. Не в том смысле, что средств нет, а в том, что они чрезвычайно неудобны и легко выводят из себя привыкшего к элегантному формату ...
printf()
программиста. Не верите? Давайте попробуем вывести обыкновенную дату в формате dd.mm.yyyy
: