Visual Studio. Обновленный вариант кода теперь работает во все трех опробованных мной компиляторах: g++, bcc32 и cl.
Blog about programming for beginners and beyond / Блог о программировании. Для начинающих и не только.
Visual Studio. Обновленный вариант кода теперь работает во все трех опробованных мной компиляторах: g++, bcc32 и cl.
Есть в шаблонах С++ интересная возможность параметризировать сами параметры шаблонов. Чтобы это могло значить?
Бывают случаи, когда параметр шаблона сам является шаблонным классом и для его инстанцирования тоже нужно указать параметр. Например, универсальная шаблонная функция для печати стандартного контейнера любого типа в поток:
template< typename C, typename E >
void print(const C<E>& v) {
copy(v.begin(), v.end(), ostream_iterator<E>(cout, " "));
cout << endl;
}И все бы ничего, но с только зрения синтаксиса С++ это неверно. Нельзя просто написать C<E>, если E сам является не определенным типом, а параметром шаблона. Правильный способ использования параметра шаблона, который в свою очередь зависит от другого параметра, должен выглядеть так: template< template<typename> class C, typename E >
void print(const C<E>& v) {
copy(v.begin(), v.end(), ostream_iterator<E>(cout, " "));
cout << endl;
}Теперь полный пример (файл template_parameter.cpp): #include <iostream>
#include <iomanip>
#include <algorithm>
#include <iterator>
#include <string>
#include <vector>
#include <list>
#include <deque>
// Я обычно не использую пространства имен "по умолчанию", но тут
// это сделано для компактности примера.
using namespace std;
// Вся изюминка тут: template<typename> или template<class>.
// Без этого параметр шаблона "С" нельзя будет параметризировать.
// в конструкции C<E>&.
template< template<typename> class C, typename E >
// Тут происходит параметризация параметра "С" параметром "E".
// Без этого класс "С" не может быть использован, так как "E"
// является не просто типом, а тоже параметром шаблона.
void print(const C<E>& v) {
// Так как класс элемента контейнера "Е" нам тут нужен как отдельный
// тип, то для этого и затеяна вся тема с параметризированными
// параметрами шаблона.
copy(v.begin(), v.end(), ostream_iterator<E>(cout, " "));
cout << endl;
}
// Тестовая программа демонстрирует, как одна функция print()
// может использоваться для печати любого контейнера
// (если, конечно, он удовлетворяет требованиям алгоритма
// copy() по наличию должных итераторов), содержащего элементы
// любого типа.
int main(int argc, char* argv[]) {
// Массив целых.
int i[5] = { 1, 2, 3, 4, 5 };
// Создаем вектор, состоящий из целых, и печатаем его.
print< vector, int >( vector<int>(i, i + 5) );
// Массив вещественных.
float f[5] = { .1, .2, .3, .4, .5 };
// Создаем вектор, состоящий из вещественных, и печатаем его.
print< vector, float >( vector<float>(f, f + 5) );
// Массив символов.
char c[5] = { 'a', 'b', 'c', 'd', 'e' };
// Создаем деку, состоящую их символов, и печатаем ее.
print< deque, char >( deque<char>(c, c + 5) );
// Массив строк в стиле С.
char* s[5] = { "a1", "b2", "c3", "d4", "e5" };
// Создаем список, состоящий из строк, и печатаем его.
print< list, string >( list<string>(s, s + 5) );
return 0;
}Компилируем.Cygwin:
g++ -o template_parameter_cygwin.exe template_parameter.cppили в Borland/Codegear Studio 2007: bcc32 /etemplate_parameter_cg2007.exe template_parameter.cppИ запускаем скомпилированный файл: 1 2 3 4 5
0.1 0.2 0.3 0.4 0.5
a b c d e
a1 b2 c3 d4 e5Отчетливо видно, что на первой строке распечатаны целые, на второй вещественные, на третьей символы, и на четвертой строки. Вы спросите, где компиляция в Visual Studio? А вот с ней вышел облом. Я пробовал скомпилировать этот пример в Visual Studio 2005 и 2008, и в обоих случаях я получал ошибки типа:
template_as_parameter.cpp(38) : error C3208: 'print' : template parameter list for class template 'std::vector' does not match template parameter list for template template parameter 'C' Из чего я сделал вывод, что микрософтовский компилятор не поддерживает подобный синтаксис.
Я был очень расстроен подобным фактом, так как в целом очень положительно отношусь кПредвосхищу вопросы типа "зачем так сложно, да еще и плохо переносимо" — все верно. Лично я бы отнес все выше описанное к "темным углам" С++, но уж больно интересно по ним полазать.cl.exe. А тут выходит, что даже борландовый компилятор это понимает, аcl.exeнет. Если кто знает, может есть ключик какой секретный для включения поддержки "хитрых и редких" возможностей С++ в компиляторе микрософта — научите, пожалуйста. Буду очень признателен.
Visual Studio. Окончательный вариант кода, чтобы работало в cl.exe, таков:template< template<typename, typename> class C, typename E >
void print(const C<E, allocator<E> >& v) {
copy(v.begin(), v.end(), ostream_iterator<E>(cout, " "));
cout << endl;
}У шаблонов стандартных контейнеров есть второй параметр, так называемый allocator. Этот параметр часто используется со значением по умолчанию, поэтому редко приходится вспоминать о нем. И как уточнил Александр, моя проблема была в том, что cl.exe требует явного указания наличия этого параметра при параметризации параметра C.cl.exe.SystemMessage. Все члены статические, так что можно работать с ними без создания экземпляра класса.Пространство имен, как обычно, ext, так что измените, если необходимо.Файл systemmessage.h:#ifndef _EXT_SYSTEM_MESSAGE_H
#define _EXT_SYSTEM_MESSAGE_H
#include <string>
namespace ext {
class SystemMessage {
public:
// Эта функция возращает код ошибки.
static int code();
// Эта функция по коду ошибки возвращает ее текстовое описание, если
// таковое предоставляется операционной системой. Если нет, то
// возвращается строка "?".
static std::string message(int code);
};
} // namespace ext
#endif // _EXT_SYSTEM_MESSAGE_HФайл systemmessage.cpp#include "SystemMessage.h"
#ifdef WIN32
#include <windows.h>
#else
#include <string.h>
#include <unistd.h>
#include <errno.h>
#endif
namespace ext {
int SystemMessage::code() {
#ifdef WIN32
return GetLastError();
#else
return errno;
#endif
}
// Если система по какой-то причине не имеет функции strerror_r,
// то придется лазить напрямую в таблицу сообщений об ошибках.
// Для этого надо при компиляции определить макрос LIBC_NO_STRERROR_R.
// Пока я видел такое только на HP-UX IA64 v2.
#ifndef WIN32
#ifndef LIBC_NO_STRERROR_R
extern "C" int sys_nerr;
extern "C" char* sys_errlist[];
#endif
#endif
std::string SystemMessage::message(int code) {
char msg[1024];
#ifdef WIN32
// Версия для Windows
FormatMessage(
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
code,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
msg,
sizeof(msg) - 1,
NULL
);
char* p = msg + strlen(msg);
// Обрезаем c конца '\r', '\n' и '.'
for(p = msg + strlen(msg) - 1;
p >= msg && (*p == '\n' || *p == '\r' || *p == '.'); --p)
*p = 0;
#elif LIBC_NO_STRERROR_R
// Если UNIX-платформа не имеет функции strerror_r, то делаем ее
// работу вручную. Пока я встретил такое только на HP-UX IA64 v2.
if (code < 0 || code >= sys_nerr)
return "?";
strncpy(msg, sys_errlist[code], sizeof(msg) - 1);
// Если сообщение об ошибке длинее чем sizeof(msg)-1, то '\0'
// не будет скопирован, поэтому добавляем его вручну.
msg[sizeof(msg) - 1] = 0;
#else
// Для нормальной UNIX-системы просто вызываем strerror_r.
if (strerror_r(code, msg, sizeof(msg) - 1) < 0)
return "?";
#endif
// Поможем компилятору по возможности оптимизировать
// возвращаемое значение как rvalue.
return std::string(msg);
}
} // namespace extТеперь посмострим это в работе. Я как-то пока не придумал, как универсально написать unit-тест для этого класса, так как предсказуемые результаты будут все равно различны для каждой платформы. А писать тесты под все платформы как-то топорно. Хочется гармонии, а тут пока ее нет. Если кто имеет идею, как универсально тестировать этот класс на всех платформах поделитесь, пожалуйста.Тестовая программа
systemmessage_test.cpp:#include <iostream>
#include <fstream>
#include "systemmessage.h"
int main(int argc, char* argv[]) {
// Пытаемся открыть заведомо несуществующий файл.
std::ifstream is("__non_existing_file__");
// Печатаем ошибку.
int error = ext::SystemMessage::code();
std::cout
<< error << ", "
<< ext::SystemMessage::message(error)
<< std::endl;
return 0;
}Компилируем в Visual Studio:cl /EHsc /Fesystemmessage_test_vs2008.exe /DWIN32 systemmessage_test.cpp systemmessage.cppЗапускаем systemmessage_test_vs2008.exe:2, The system cannot find the file specifiedПолучили примерно ожидаемое виндовое сообщение об ошибке.g++ -o systemmessage_test_cygwin.exe systemmessage_test.cpp systemmessage.cppЗапускаем systemmessage_test_cygwin.exe:2, No such file or directoryПолучили сообщение об ошибке в стиле UNIX.Повторюсь, в данном классе нет ничего удивительного и сложного. Просто это весьма универсальный и переносимый исходник.И небольшая ремарка. В мире UNIX существует два диалекта функции
strerror_r: XSI-версия (когда определен макрос _XOPEN_SOURCE, и он равен 600) и GNU-версия (доступная в libc, начиная с версии 2.0). Разница в том, что первая (XSI-версия) просто кладет сообщение об ошибке в предоставленный буфер и также возвращает код успешности или неуспешности своей работы в виде int'а. Нормальный UNIX-подход. Вторая версия (GNU) возвращает не int, а, собственно, указатель на строку с ошибкой, причем указываеть он может как на предоставленный функции буфер, так и куда-то еще, например, на какой-то внутренний буфер. Данный класс рассчитан на работу с XSI-версией функции strerror_r. Поэтому, если вдруг при компиляции этого класс на UNIX-системах вы получите сообщение об ошибке в использовании этой функции, то определите макрос _XOPEN_SOURCE в значение 600 (-D_XOPEN_SOURCE=600 для компилятора), тем самым будет принудительно использоваться XSI-версия этой болезной функции.
Интересно, что настоящим автором Нортон Коммандера является далеко не сам Питер Нортон, а John Socha. Именно он создал изначальную версию, которая называлась VDOS (Virtual DOS), еще будучи студентом. Следуя именно идее “виртуального ДОСа”, первая версия коммандера имела файловые панели только на половину экрана — вторая нижняя часть была "виртуальным окном" в ДОС. Сейчас это окно обычно сокращают до одной командной строки. Затем Джон Сόха присоединился к Peter Norton Computing, и программа начала свое триумфальное шествие по планете под именем Norton Commander. Последней версией коммандера, вышедшей из под рук Джона, была версия 3.0. Именно она является "классической" и именно её знало большинство российских пользователей IBM PC. Но эта версия была лебединой песней коммандера. Питер Нортон продал компанию Symantec'у, а Джон Сόха уволился, прекратив работу над коммандером. После версии 3.0 в Symantec выпустили еще пару версий, но время уходило. Оригинальный интерфейс версии 3.0 был "улучшен", программа заметно потолстела и замедлилась, а на пятки уже наступали Дос Навигатор и Волков Коммандер, а в новом 32-битном мире — первые версии FAR и Total (Windows) Commander.На мой взгляд, именно представление файловой структуры в виде двух панелей с возможностью адресовать файлы с одной панели на другую, дополненная мгновенным редактором и просмотрщиком, является наиболее удобной для программистской работы, когда необходимо прыгать между десятком файлов одновременно, чего-то временно скопировать/переименовать/удалить, тут что-то быстренько отредактировать, в другом месте поискать и т.д. С трудом могу представить все эти действия через однопанельный файловый а-ля explorer, где только для копирования файла надо сначала мышкой на него указать, взять "на копирование", открыть второй explorer с местом назначения и сделать туда "Paste". Например, замечаю на собой постоянно, если мне надо подправить какие-либо настройки проекта в Visual Studio (например, пути), то мне гораздо быстрее переключиться в FAR, там через F4 открыть файл проекта, найти нужное место и исправить прямого в тексте конфигурации, чем ползать через окошки и менюшки самой IDE. Чего уж говорить об удобстве встроенного редактора, особенно если установлена подсветка синтаксиса. Я много раз пытался заставить себя по F4 вызывать внешний редактор, типа Notepad++, но все равно скатывался до встроенного, ибо он вызывается мгновенно. Notepad++, конечно, отличная программа, но в ней я только делаю “сложную” поиск-замену, когда нужны регулярные выражения.Вообще, история создания эпохального файлового менеджера весьма интересна.
Мир UNIX'а тоже не обделен хорошими двухпанельными файловыми менеджерами, реально ускоряющими процесс, когда надо сотни раз повторять cd/ls/cp/mv/cd/ls/cp/mv..., разруливая какой-нибудь завал на файловой системе. Midnight Commander знают все. Радостно, что за последние несколько месяцев снова началась активная работа над проектом, знамя которого лежало без движения уже несколько лет. Но есть еще один заслуживающий внимания проект. Неутомимый Сергей Вакуленко создал и развивает Bash Commander. Сейчас это патч к официальной версии Bash, но Сергей бьется за включения его в общее дерево. Этот патч ненавязчиво добавляет в Bash то, что там так не хватает: при нажатии на Ctrl-O (^O) появляются заветные две панели. То есть пока идет неспешная программерская работа в стиле make/vi, то все как обычно в командной строке шелла, но как только надо муторно повозиться с множеством файлов, то нажимаем Ctrl-O, и далее привычные панели с клавишам F1-F10. Интерфейс, конечно, крайне минималистический и не имеет всех наворотов MC, но есть один плюс - крайне простая сборка. Везде, где собирается Bash, там можно собрать и Bash Commander, чего нельзя сказать MC с длинным списков зависимостей, который просто собирается разве что под линуксом, а вот на динозаврах типа AIX'а или HP-UX'а становится грустно. Функциональность Bash Commander’а легко расширяется при помощи самих же скриптов на Bash. Надеюсь, что работу Сергея таки включат в официальную версию Bash.Мне кажется, что наш постсоветский IBM-PC'шный мир привил некоторым из нас не самую плохую привычку к двухпанельным текстовым файловым менеджерам, не так ли?
1234567890 секунд. Также, это будет пятница 13-е!К сожалению для Москвы этот день будет не столь особым, так как там это произойдет уже 14-го в 02:31:30.Это, наверное, как наблюдать затмение, в некотором цифровом виде.
std::ifstream is("testfile.txt");
std::string val(
(std::istream_iterator<char>(is)),
std::istream_iterator<char>()
);Обратите внимание на вроде "излишние" скобки вокруг первого параметра конструктора строки val.Скотт Мейерс в книге "Эффективное использование STL. Библиотека программиста" в Совете 6 "Остерегайтесь странностей лексического разбора C++" (стр. 42, изд. "Питер" 2002) дает исчерпывающее объяснение этого "феномена". Ответ крайне меня опечалил, так как вскрыл некоторую нелогичность и корявость в целом стройного и красивого языка С++ в данном вопросе. Очевидно, что причины этого в сохранения в С++ обратной совместимости с С, но от этого не легче.Итак, давайте разберемся по порядку (чтобы меня не обвинили в плагиате, сразу скажу, что я буду следовать примерному тексту Мейерса, так как он дал великолепное объяснение с примерами, и изобретать велосипед в данном случае было бы неразумно). Как мы предполагали, код std::istream_iterator<char>(is) создает экземпляр потокового итератора, привязанного к потоку is. И все бы ничего, если такая конструкция используется как самостоятельное объявление. Вся проблема в именно в использовании такого выражения в контексте вызова функции (в данном случае, конструктора), то есть в качестве параметра. Мейерс приводит следующий пример:
int f(double d);Это команда объявления функции f, которая получает double и возвращает int. Тоже самое происходит и в следующей строке. Круглые скобки вокруг имени параметра d не нужны, поэтому компилятор их игнорирует:
int f(double (d)); // То же; круглые скобки вокруг d игнорируютсяТеперь третий вариант объявления той же функции. В нем имя параметра просто не указано: int f(double); // То же; имя параметра не указаноТри такие формы объявления знакомы всем, хотя про возможность заключения параметра в скобки знают не все (может просто потому, что это очевидно лишние по логике вещей скобки). Теперь можно рассмотреть еще три объявления функции. В первом объявляется функция g с параметром — указателем на функцию, которая вызывается без параметров и возвращает double:
int g(double (*pf)()); // Функции g передается указатель на функциюТо же самое можно сформулировать иначе. Единственное различие заключается в том, что pf объявляется в синтаксисе без указателей (допустимом как в С, так и в С++): int g(double pf()); // То же; pf неявно интерпретируется как указательКак обычно, имена параметров могут опускаться, поэтому возможен и третий вариант объявления g без указания имени pf: int g(double()); // То же; имя параметра не указаноОбратите внимание на различия между круглыми скобками вокруг имени параметра (например, параметра d во втором объявлении f) и стоящими отдельно (как в этом примере). Круглые скобки, в которые заключено имя параметра, игнорируются, а стоящие отдельно, обозначают присутствие списка параметров; они сообщают о присутствии параметра, который является указателем на функцию. Теперь вернемся к оригинальному примеру:
std::ifstream is("testfile.txt");
std::string val(
std::istream_iterator<char>(is),
std::istream_iterator<char>()
);Сейчас я намеренно убрал таинственные "лишние" скобки вокруг первого параметра.Что же перед нами тут? Совершенно не то, о чем мы думали изначально. Перед нами объявление функции
val, возвращающей тип std::string. Функция получает два параметра:is, относится к типу istream_iterator<char>. Лишние круглые скобки вокруг is игнорируются. istream_iterator<char>.В грамматике имеется неоднозначность, когда инструкция может быть выражением, так и объявлением. Если выражение с явным преобразованием типов в стиле вызова функции (_expr.type.conv_) является крайним слева, то оно может быть неотличимо от объявления, в котором первый оператор объявления начинается с открытой круглой скобки "(". В этом случае инструкция рассматривается как объявление. — [C++03] п.6.8Так что же делают эти магические скобки вокруг первого параметра конструктора?
std::ifstream is("testfile.txt");
std::string val(
(std::istream_iterator<char>(is)),
std::istream_iterator<char>()
);А вот что — объявления формальных параметров не могут заключаться в круглые скобки, я вот заключить в круглые скобки аргумент при вызове функции можно. Вот эти круглые скобки и помогают компилятору решить неоднозначность в нужную нам сторону (а не как положено по стандарту по умолчанию) и точно указать, что перед нами именно использование параметра функции при ее вызове, а не при объявлении. Соглашусь, от этого может слегка заболеть голова, причем совершенно без причины.
Как написал Герб Саттер в книге "Новые сложные задачи на С++" (он тоже посвятил этому вопросу целую главу, “Задача 23. Инициализация ли это?”, стр. 192, изд. “Вильямс”), что такие моменты синтаксиса С++ являются его "темными углами", и их стоит избегать. Рассмотренный пример можно упростить, объявив итератор отдельно, а не прямо в тексте вызова конструктора, тем самым не заходить в "темный угол". Не так элегантно, зато просто и понятно:
std::ifstream is("testfile.txt");
std::istream_iterator<char> begin(is);
std::istream_iterator<char> end;
std::string val(begin, end);Читал я недавно, как Линус Торвальдс полоскал С++ за неоправданную языковую сложность. "C++ is a horrible language!", — сказал Линус. Может он и прав.Мыши плакали, кололись, но продолжали грызть С++.
php это делается так: $lines = file_get_contents("textfile.txt");или так, медленно и неэффективно, по-старинке: $lines = join("", file("textfile.txt"));Задумался я, как бы это так элегантно одним оператором сделать в С++. Естественно, хочется, чтобы это работало приемлемо быстро для больших файлов (например, от одного мегабайта и больше). Первое, что сходу приходит в голову (универсальная кондовая классика):
std::ifstream is("testfile.txt");
std::string v;
char buf[N];
while (is) {
is.read(buf, sizeof(buf));
v.append(buf, is.gcount());
}где с размером буфера N можно еще проиграться, выбрав оптимальный.Первое улучшение, приходящее в голову — это распределить заранее внутренний буфер для строки, минимизировав количество перераспределений буфера в процессе чтения. Для этого мы, естественно, должны заранее знать размер читаемого файла. Пусть это будет 1 мегабайт (1024*1024).
std::ifstream is("testfile.txt");
std::string v;
// Резервируем внутренний буфер std::string на указанный
// размер в один мегабайт.
v.reserve(1024*1024);
char buf[N];
while (is) {
is.read(buf, sizeof(buf));
v.append(buf, is.gcount());
}Далее на сцену выходят STL-алгоритмы и потоковые итераторы. Берем типовой пример использования алгоритма stl::copy, который есть практически в любой книге по С++: std::ifstream is("testfile.txt");
std::string v;
// Сброс данного флага необходим для отключения пропуска пробельных
// символов при форматном вводе через поток.
is.unsetf(std::ios::skipws);
std::copy(
std::istream_iterator<char>(is),
std::istream_iterator<char>(),
std::back_inserter(v)
);Сразу скажу, это крайне медленный метод. Первым, приходящим в голову улучшением, как всегда, является предварительное распределение буфера приемной строки: std::ifstream is("testfile.txt");
std::string v;
// Резервируем внутренний буфер std::string на указанный
// размер в один мегабайт.
v.reserve(1024*1024);
is.unsetf(std::ios::skipws);
std::copy(
std::istream_iterator<char>(is),
std::istream_iterator<char>(),
std::back_inserter(v)
);В рассмотренном методе видно, что сначала данные изымаются из потока потоковым итератором istream_iterator, а потом кладутся через итератор строки back_inserter в саму строку. Двойная работа. Есть метод лучше — класть данные из потока напрямую в строку, используя один из специальных конструкторов класса std::string: std::ifstream is("testfile.txt");
is.unsetf(std::ios::skipws);
// Самое интересное происходит тут: создается переменная "v"
// через конструктор, работающий напрямую с итераторами потока,
// и данные напрямую поступают во внутренний буфер строки.
std::string v(
(std::istream_iterator<char>(is)),
std::istream_iterator<char>()
);Опытный читатель заметит казалось бы ненужные обрамляющие скобки вокруг первого параметра. Сразу скажу — без них работать не будет, а будет ошибка компиляции. Тут мы касаемся одного из "темных углов" С++. Это не самый очевидный вопрос, поэтому я посвятил ему отдельную статью.Уже лучше, но двигаемся дальше. В потоках ввода есть специальные итераторы
istreambuf_iterator, которые работают напрямую с внутренними буферами потока в обход всех высокоуровневых функций форматирования и выходного преобразования. Именно по этому для них вызов функции unsetf будет уже не нужен: std::ifstream is("testfile.txt");
std::string v;
// Опциональное резервирование буфера приемной строки.
v.reserve(1024*1024);
std::copy(
std::istreambuf_iterator<char>(is),
std::istreambuf_iterator<char>(),
std::back_inserter(v)
);И теперь вариант через конструктор класса std::string: std::ifstream is("testfile.txt");
std::string v(
(std::istreambuf_iterator<char>(is)),
std::istreambuf_iterator<char>()
);Мы уже близки к идеалу. Теперь встроим создание объекта std::ifstream прямо в код создания строки: std::string v(
(std::istreambuf_iterator<char>(
std::ifstream("testfile.txt")
)),
std::istreambuf_iterator<char>()
);Мы уже в миллиметре от идеала, но в приведенном примере есть одно большое "но". Вызов std::ifstream("testfile.txt") прямо в вызове конструктора создает временный объект, который по стандарту языка всегда является константой, а первый параметр конструктора ожидает принять не константный параметр, поэтому "строгий" компилятор типа gcc скорее всего выдаст ошибку компиляции, а менее "строгий", например cl.exe от Майкрософта на такой вызов не ругается. Но мы не можем принять такое не универсальное решение, поэтому изменим код, чтобы параметр создавался динамически в куче, а для автоматического его удаления будет использоваться std::auto_ptr: std::string v(
(std::istreambuf_iterator<char>(
*(std::auto_ptr<std::ifstream>(
new std::ifstream("testfile.txt")
)).get()
)),
std::istreambuf_iterator<char>()
);
Этот код должен работать в любом стандартном компиляторе С++.
Оглядимся назад. У нас столько вариантов — какой выбрать? Для начала, скорость. Надо понять, какой вариант работает банально быстрее. Для этого я собрал все эти варианты в тестовую программу (конечно, с использованием Google Test Framework).
Как я уже писал, вы можете скачать мою модификацию этой библиотеки, которая сокращена до двух необходимых файловФайлgtest/gtest.hиgtest-all.cc.
filereader_unittest.cpp:#include <gtest/gtest.h>
#include <iostream>
#include <streambuf>
#include <istream>
#include <fstream>
#include <ios>
#include <iomanip>
#include <string>
#include <vector>
#include <memory>
#include <cstdlib>
// Управляющий класс для нашей среды тестирования.
class Env: public testing::Environment {
public:
// Размер тестового файла: 1 мегабайт.
static int testfile_sz() { return 1024 * 1024; }
// Имя тестового файла.
static const char* testfile() { return "testfile"; }
protected:
// Эта функция вызывается один раз в начале тестирования.
// Она создает тестовый файл.
void SetUp() {
std::string dummy(testfile_sz(), 'x');
std::ofstream os(testfile());
os.write(dummy.c_str(), dummy.length());
}
// Эта функция вызывается один раз после всех тестов.
// Она удаляет тестовый файл.
void TearDown() {
std::remove(testfile());
}
};
// Функция, реализующая классический метод чтения файла кусками
// заданной длины N. При необходимости производится предварительное
// распределение буфера приемной строки.
void rawRead(int N, bool reserve) {
std::ifstream is(Env::testfile());
std::string v;
if (reserve)
v.reserve(Env::testfile_sz());
char* buf = new char[N];
while (is) {
is.read(buf, sizeof(buf));
v.append(buf, is.gcount());
}
delete[] buf;
// На всякий случай проверяем размер считанного файла.
EXPECT_EQ(Env::testfile_sz(), v.length());
}
// Классическое чтение с буфером в 100 байт.
TEST(ReaderTest, raw_100) {
rawRead(100, false);
}
// Классическое чтение с буфером в 1 килобайт.
TEST(ReaderTest, raw_1024) {
rawRead(1024, false);
}
// Классическое чтение с буфером в 10 килобайт.
TEST(ReaderTest, raw_10240) {
rawRead(10240, false);
}
// Классическое чтение с буфером в 10 килобайт с предварительным
// распределением буфера приемной строки.
TEST(ReaderTest, raw_reserve_10240) {
rawRead(10240, true);
}
// Функция, реализующая чтение через итератор istream_iterator.
// При необходимости производится предварительное распределение
// буфера приемной строки.
void check_istream_iterator(bool reserve) {
std::ifstream is(Env::testfile());
std::string v;
if (reserve)
v.reserve(Env::testfile_sz());
// Принудительное игнорирование пропуска пробельных символов.
// С этим флагом двоичные данные будут читаться неверно.
is.unsetf(std::ios::skipws);
std::copy(
std::istream_iterator<char>(is),
std::istream_iterator<char>(),
std::back_inserter(v)
);
// На всякий случай проверяем размер считанного файла.
EXPECT_EQ(Env::testfile_sz(), v.length());
}
// Тестируем работу через istream_iterator.
TEST(ReaderTest, istream_iterator) {
check_istream_iterator(false);
}
// Тестируем работу через istream_iterator с предварительным
// распределением буфера приемной строки.
TEST(ReaderTest, istream_iterator_reserve) {
check_istream_iterator(true);
}
// Тестируем работу через istream_iterator при прямом
// вызове конструктора строки, который берет данные напрямую
// из итераторов.
TEST(ReaderTest, istream_iterator_tostring) {
std::ifstream is(Env::testfile());
is.unsetf(std::ios::skipws);
std::string v(
(std::istream_iterator<char>(is)),
std::istream_iterator<char>()
);
// На всякий случай проверяем размер считанного файла.
EXPECT_EQ(Env::testfile_sz(), v.length());
}
// Функция, реализующая чтение через итератор istreambuf_iterator.
// При необходимости производится предварительное распределение
// буфера приемной строки. Для данного метода сброс флага
// std::ios::skipws не нужен, так как этот итератор работает
// на более низком уровне.
void check_istreambuf_iterator(bool reserve) {
std::ifstream is(Env::testfile());
std::string v;
if (reserve)
v.reserve(Env::testfile_sz());
std::copy(
std::istreambuf_iterator<char>(is),
std::istreambuf_iterator<char>(),
std::back_inserter(v)
);
// На всякий случай проверяем размер считанного файла.
EXPECT_EQ(Env::testfile_sz(), v.length());
}
// Тестируем работу через istreambuf_iterator.
TEST(ReaderTest, istreambuf_iterator) {
check_istreambuf_iterator(false);
}
// Тестируем работу через istreambuf_iterator с предварительным
// распределением буфера приемной строки.
TEST(ReaderTest, istreambuf_iterator_reserve) {
check_istreambuf_iterator(true);
}
// Тестируем работу через istreambuf_iterator при прямом
// вызове конструктора строки, который берет данные напрямую
// из итераторов.
TEST(ReaderTest, istreambuf_iterator_tostring) {
std::ifstream is(Env::testfile());
std::string v(
(std::istreambuf_iterator<char>(is)),
std::istreambuf_iterator<char>()
);
// На всякий случай проверяем размер считанного файла.
EXPECT_EQ(Env::testfile_sz(), v.length());
}
#ifdef WIN32
// Этот тест аналогичен тесту istreambuf_iterator_tostring
// за исключение создания объекта потока прямо в вызове
// конструктора строки. Работает только в cl.exe, так как
// "стандартный" компилятор запрещает передавать временные
// объекты по неконстантной ссылке, а cl.exe почему-то это
// разрешает.
TEST(ReaderTest, istreambuf_iterator_tostring_short) {
std::string v(
(std::istreambuf_iterator<char>(
std::ifstream(Env::testfile())
)),
std::istreambuf_iterator<char>()
);
// На всякий случай проверяем размер считанного файла.
EXPECT_EQ(Env::testfile_sz(), v.length());
}
#endif
// Финальный метод. Конструктор строки берет данные напрямую
// из итератора istreambuf_iterator. Объект потока создается
// динамически прямо в коде вызова конструктора строки через
// std::auto_ptr.
TEST(ReaderTest, istreambuf_iterator_tostring_short_auto_ptr) {
std::string v(
(std::istreambuf_iterator<char>(
*(std::auto_ptr<std::ifstream>(
new std::ifstream(Env::testfile())
)).get()
)),
std::istreambuf_iterator<char>()
);
// На всякий случай проверяем размер считанного файла.
EXPECT_EQ(Env::testfile_sz(), v.length());
}
// Запуск тестов.
int main(int argc, char **argv) {
// Инициализация нашей тестовой среды.
testing::AddGlobalTestEnvironment(new Env);
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}Для компиляции вам необходимы файлыДля начала скомпилируем в Visual Studio 2008:gtest/gtest.hиgtest-all.cc(см. выше).
cl /Fefilereader_vs2008.exe /DWIN32 /O2 /arch:SSE2 /I. /EHsc filereader_unittest.cpp gtest-all.ccЗапускаем: filereader_vs2008.exe --gtest_print_timeОпция --gtest_print_time указывает Google Test выводить время работы каждого теста.Результат: [==========] Running 12 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 12 tests from ReaderTest
[ RUN ] ReaderTest.raw_100
[ OK ] ReaderTest.raw_100 (141 ms)
[ RUN ] ReaderTest.raw_1024
[ OK ] ReaderTest.raw_1024 (94 ms)
[ RUN ] ReaderTest.raw_10240
[ OK ] ReaderTest.raw_10240 (109 ms)
[ RUN ] ReaderTest.raw_reserve_10240
[ OK ] ReaderTest.raw_reserve_10240 (94 ms)
[ RUN ] ReaderTest.istream_iterator
[ OK ] ReaderTest.istream_iterator (359 ms)
[ RUN ] ReaderTest.istream_iterator_reserve
[ OK ] ReaderTest.istream_iterator_reserve (344 ms)
[ RUN ] ReaderTest.istream_iterator_tostring
[ OK ] ReaderTest.istream_iterator_tostring (281 ms)
[ RUN ] ReaderTest.istreambuf_iterator
[ OK ] ReaderTest.istreambuf_iterator (141 ms)
[ RUN ] ReaderTest.istreambuf_iterator_reserve
[ OK ] ReaderTest.istreambuf_iterator_reserve (125 ms)
[ RUN ] ReaderTest.istreambuf_iterator_tostring
[ OK ] ReaderTest.istreambuf_iterator_tostring (78 ms)
[ RUN ] ReaderTest.istreambuf_iterator_tostring_short
[ OK ] ReaderTest.istreambuf_iterator_tostring_short (67 ms)
[ RUN ] ReaderTest.istreambuf_iterator_tostring_short_auto_ptr
[ OK ] ReaderTest.istreambuf_iterator_tostring_short_auto_ptr (78 ms)
[----------] 12 tests from ReaderTest (1891 ms total)
[----------] Global test environment tear-down
[==========] 12 tests from 1 test case ran. (1906 ms total)
[ PASSED ] 12 tests.
Теперь в gcc 3.4.4: g++ -O2 -funroll-all-loops -fomit-frame-pointer -msse2 -mtune=nocona -I. -o filereader_gcc344.exe filereader_unittest.cpp gtest-all.ccЗапускаем: filereader_gcc344.exe --gtest_print_timeРезультат: [==========] Running 11 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 11 tests from ReaderTest
[ RUN ] ReaderTest.raw_100
[ OK ] ReaderTest.raw_100 (647 ms)
[ RUN ] ReaderTest.raw_1024
[ OK ] ReaderTest.raw_1024 (649 ms)
[ RUN ] ReaderTest.raw_10240
[ OK ] ReaderTest.raw_10240 (647 ms)
[ RUN ] ReaderTest.raw_reserve_10240
[ OK ] ReaderTest.raw_reserve_10240 (644 ms)
[ RUN ] ReaderTest.istream_iterator
[ OK ] ReaderTest.istream_iterator (2141 ms)
[ RUN ] ReaderTest.istream_iterator_reserve
[ OK ] ReaderTest.istream_iterator_reserve (2131 ms)
[ RUN ] ReaderTest.istream_iterator_tostring
[ OK ] ReaderTest.istream_iterator_tostring (1115 ms)
[ RUN ] ReaderTest.istreambuf_iterator
[ OK ] ReaderTest.istreambuf_iterator (1124 ms)
[ RUN ] ReaderTest.istreambuf_iterator_reserve
[ OK ] ReaderTest.istreambuf_iterator_reserve (1120 ms)
[ RUN ] ReaderTest.istreambuf_iterator_tostring
[ OK ] ReaderTest.istreambuf_iterator_tostring (76 ms)
[ RUN ] ReaderTest.istreambuf_iterator_tostring_short_auto_ptr
[ OK ] ReaderTest.istreambuf_iterator_tostring_short_auto_ptr (74 ms)
[----------] 11 tests from ReaderTest (10371 ms total)
[----------] Global test environment tear-down
[==========] 11 tests from 1 test case ran. (10396 ms total)
[ PASSED ] 11 tests.
Давайте проанализируем результаты.
Мы не будем сравнивать абсолютные времена компиляторов друг против друга. Сейчас не об этом.
Вывод первый. Предварительное резервирование буфера приемной строки (метод reserve()) не дает никакого эффекта в нашем случае. Может это из-за того, что стратегия расширения буфера при простом линейном добавлении данных итак весьма эффективна в классе std::string.
Вывод второй. Размер буфера чтения в методе чтения файла явными кусками установленного размера не дал четкой картины. Не очевидно, какой размер буфера может быть потенциально оптимальным. Тут может и дисковый кэш повлиял, может внутреннее буферизирование в классе std::ifstream, может что-то еще.
Вывод третий. Работа через итератор istream_iterator является крайне медленной. Возможно это связано с накладными расходами на форматные преобразования, производимые данным классом и совершенно ненужные в нашей задаче. Для реального использования данный метод практически непригоден.
Вывод четвертый. Использование конструктора класса std::string, работающего напрямую с итераторами потока, заметно быстрее, чем использование алгоритма std::copy (ReaderTest.istream_iterator заметно медленнее ReaderTest.istream_iterator_tostring и ReaderTest.istreambuf_iterator заметно медленнее ReaderTest.istreambuf_iterator_tostring). И понятно почему — данные напрямую поступают в буфер строки без ненужного промежуточного копирования.
Вывод пятый (основной). Метод чтения через итератор istreambuf_iterator с использованием конструктора строки, работающего напрямую с итераторами потока (тест ReaderTest.istreambuf_iterator_tostring для "нестрогого" компилятора и тест ReaderTest.istreambuf_iterator_tostring_auto_ptr для компилятора, следующего стандартам), является весьма эффективным и может конкурировать с ручным блочным чтением. Конечно, текст данного метода весьма непрост и может запутан для понимания на первый взгляд, особенно для начинающих, а блочное чтения прозрачно и ясно, но при почти равной эффективности этих методов нет причин отказываться от работы через итераторы, так как данный метод весь фактически предоставляется библиотекой STL, а значит быть может оптимизирован независимо, без затрагивания кода уже использующей его программы.
Другие посты по теме: