top of page
Поиск

Пять популярных мифов про C++, часть 2

  • Фото автора: Илья Лавренов
    Илья Лавренов
  • 12 февр. 2015 г.
  • 10 мин. чтения

4.2 Разделённое владение shared_ptr

Не у каждого объекта может быть один владелец. Нам надо убедиться, что объект уничтожен и освобождён, когда исчезает последняя ссылка на него. Таким образом, нам необходима модель разделённого владения объектом. Допустим, у нас есть синхронная очередь, sync_queue, для общения между задачами. Отправитель и получатель получают по указателю на sync_queue:

void startup()

{

sync_queue* p = new sync_queue{200}; // опасность!

thread t1 {task1,iqueue,p}; // task1 читает из *iqueue и пишет в *p

thread t2 {task2,p,oqueue}; // task2 читает из *p и пишет в *oqueue

t1.detach();

t2.detach();

}

Предполагается, что task1, task2, iqueue и oqueue уже где-то были соответствующим образом определены и прошу прощения за то, что thread переживёт область видимости, где они были созданы (посредством detatch()). Вопрос: кто удалит sync_queue, созданные в startup()? Ответ: тот, кто последний будет использовать sync_queue. Это классический случай, когда требуется сборка мусора. Изначально сборка подсчитывала указатели: нужно хранить количество использований объекта, и в тот момент, когда счётчик обнуляется, удалять его. Множество современных языков работают так, а С++11 поддерживает эту идею через shared_ptr. Пример превращается в:

void startup()

{

auto p = make_shared<sync_queue>(200); // создать sync_queue и вернуть указатель stared_ptr на неё

thread t1 {task1,iqueue,p}; // task1 читает из *iqueue и пишет в *p

thread t2 {task2,p,oqueue}; // task2 читает из *p и пишет в *oqueue

t1.detach();

t2.detach();

}

Теперь деструкторы task1 и task2 могут уничтожить их shared_ptr (и в большинстве правильно построенных систем так и сделают), и последнее, что нужно сделать – уничтожить sync_queue. Это просто и довольно эффективно. Никакой сложной системы. Что важно, она не просто возвращает память, связанную с sync_queue. Она возвращает объект синхронизации (мьютекс, блокировку, что угодно), встроенный в sync_queue, чтобы синхронизировать две нити, выполняющие две задачи. Это не просто управление памятью, это управление ресурсами. Этот «скрытый» объект синхронизации обрабатывается так же, как хендлы файлов и потоков в предыдущем примере. Можно попробовать избавиться от использования shared_ptr, введя уникального владельца в какой-либо области видимости, заключающей в себе задачу, но это не всегда просто сделать – поэтому в С++11 есть и unique_ptr (для одиночного владения) and shared_ptr (для разделённого владения).

4.3 Типобезопасность

Я говорил пока о сборке мусора в контексте управления ресурсами. Но есть ещё и типобезопасность. У нас есть операция delete, которую можно применить неправильно. Пример:

X* p = new X;

X* q = p;

delete p;

// …

q->do_something(); // память, отведённая *p, могла быть перезаписана

Не надо так делать. Непосредственное применение delete опасно и не нужно в обычных случаях. Оставьте удаления классам, управляющим ресурсами — string, ostream, thread, unique_ptr и shared_ptr. Там удаления аккуратно отслеживаются.

4.4 Итог: идеалы управления ресурсами

С моей точки зрения, сборка мусора – последнее средство для управления ресурсами, а не решение задачи и не идеал.

1. Используйте подходящие абстракции, которые рекурсивно и неявно обслуживают свои ресурсы. Отдавайте предпочтение им, а не переменным в определённой области видимости.

2. Когда вам необходимо использовать указатели и ссылки, используйте умные указатели — unique_ptr и shared_ptr

3. Если ничего не помогает (например, ваш код – часть программы, которая запуталась в указателях, и не использует стратегию, поддерживаемую языком, для управления ресурсами и обработки ошибок), попробуйте обрабатывать ресурсы, не относящиеся к памяти, вручную, и включайте сборку мусора для обработки неизбежных утечек памяти.

5. Миф 4: для эффективности необходимо писать низкоуровневый код

Многие верят в то, что эффективный код обязан быть низкоуровневым. Некоторые даже верят, что низкоуровневый код обязательно эффективен. («Если оно такое уродливое, наверняка оно быстрое! Кто-то потратил кучу времени и своего таланта для создания этой штуковины!»). Конечно, можно писать эффективный код на низком уровне, и некоторый код необходимо делать низкоуровневым для работы с машинными ресурсами. Измеряйте, однако, стоит ли это ваших усилий. Современные компиляторы С++ очень эффективные, а архитектура современных машин чрезвычайно сложна. При необходимости такой низкоуровневый код необходимо прятать за интерфейсом для удобства. Часто сокрытие низкоуровневого кода за высокоуровневым интерфейсом способствует оптимизации. Там, где важна эффективность, сначала попробуйте достичь её, выразив идею на высоком уровне, не кидайтесь сразу на биты и указатели.

5.1 qsort() в С

Простой пример. Если вам надо отсортировать набор чисел с плавающей точкой по убыванию, вы могли бы написать для этого код. Но если у вас нет экстремальных требований (например, чисел больше, чем может уместиться в памяти), это было бы наивно. За десятилетия мы сделали библиотеки с алгоритмами сортировки с приемлемой скоростью работы. Мне меньше всего нравится qsort() из стандартной ISO библиотеки C:

int greater(const void* p, const void* q) // трёхстороннее сравнение

{

double x = *(double*)p; // получить значение double с адреса p

double y = *(double*)q;

if (x>y) return 1;

if (x<y) return -1;

return 0;

}

void do_my_sort(double* p, unsigned int n)

{

qsort(p,n,sizeof(*p),greater);

}

int main()

{

double a[500000];

// … fill a …

do_my_sort(a,sizeof(a)/sizeof(*a)); // передать указатель и количество элементов

// …

}

Если вы не программируете на С, или если вы в последнее время не использовали qsort, потребуется кое-что объяснить; qsort принимает 4 аргумента

— указатель на последовательность байтов

— количество элементов

— размер элемента

— функция, сравнивающая два элемента, которые передаются как указатели на их первые байты

Этот интерфейс скрывает информацию. Мы сортируем не байты – мы сортируем double, но qsort этого не знает, поэтому нам надо предоставить информацию о том, как сравнивать double, и сколько байтов в double. Конечно, компилятор знает такие вещи. Но низкоуровневый интерфейс qsort не позволяет компилятору воспользоваться этой информацией. Необходимость указывать такую простую информацию ведёт к ошибкам. Не перепутал ли я два целых аргумента qsort? Если перепутаю, компьютер этого не заметит. Соответствует ли моя compare() соглашениям в C для трёхстороннего сравнения? Если вы посмотрите на промышленную реализацию qsort (рекомендую), вы увидите, сколько усилий приложено для компенсации недостатка информации. К примеру, довольно трудно произвести смену местами элементов, заданных в виде количества байт, чтобы это было так же эффективно, как смена местами пары double. Затратные непрямые вызовы функции сравнения могут быть устранены компилятором только в том случае, если он применит распространение констант для указателей на функции.

5.2 sort() в C++

Сравним qsort с его эквивалентом sort из С++

void do_my_sort(vector<double>& v)

{

sort(v,[](double x, double y) { return x>y; }); // сортировка v по убыванию

}

int main()

{

vector<double> vd;

// … fill vd …

do_my_sort(v);

// …

}

Здесь требуется меньше объяснений. Вектору известен его размер, и нам не надо явно указывать количество элементов. Тип элементов не теряется, и не нужно помнить об их размере. По умолчанию, sort сортирует по возрастанию, поэтому пришлось задать критерий сравнения, как и для qsort. Здесь он передан в качестве лямбда-выражения, сравнивающего два double при помощи >. И так получилось, что эта лямбда тривиальным образом инлайнится всеми компиляторами С++, что я знаю, поэтому сравнение превращается в одну машинную операцию «больше, чем» — никаких неэффективных вызовов функции.

Я использовал контейнерную версию sort, чтобы не задавать итераторы явно, то есть, чтобы не писать:

std::sort(v.begin(),v.end(),[](double x, double y) { return x>y; });

Можно пойти дальше и использовать объект сравнения С++14:

sort(v,greater<>()); // сортировка v по убыванию

Какая из версий быстрее? Можно скомпилировать версию qsort как С, так и С++ без всякий различий в быстродействии, поэтому это будет скорее сравнением стилей программирования, а не языков. Библиотечные реализации используют один алгоритм для sort и qsort, поэтому это сравнение стилей программирования, а не алгоритмов. Конечно, у разных библиотек и компиляторов будут разные результаты, но для каждой реализации будет видна разумная реакция на разные уровни абстракции.

Я недавно прогнал примеры, и увидел, что sort в 2.5 раза быстрее, чем qsort. Это может меняться от компилятора к компилятору и от компьютера к компьютеру, но ни разу у меня qsort не выиграл у sort. Иногда sort выполнялся в 10 раз быстрее. Почему? В стандартной библиотеке С++ sort явно выше уровнем, чем qsort, при этом более гибкий и общий. Он типобезопасен и параметризован на типе хранения, типе элементов и критерию сортировки. Никаких указателей, размеров, байтов. Библиотека STL, к которой принадлежит sort, старается не выбрасывать никакой информации. Это приводит к превосходному инлайнингу и хорошей оптимизации.

Обобщение и высокоуровневый код могут выигрывать у низкоуровневого. Не всегда, но сравнение sort/qsort – это не единичный пример. Всегда начинайте с высокогоуровневой, точной и типобезопасной версии решения. Оптимизируйте по необходимости.

6. Миф 5: С++ предназначен для больших и сложных программ

С++ — объёмный язык. Размер определений схож с С# и Java. Но это не значит, что вам нужно знать каждую деталь, чтобы использовать его, или использовать все функции непосредственно в каждой программе. Вот пример использования основных компонент из стандартной библиотеки:

set<string> get_addresses(istream& is)

{

set<string> addr;

regex pat { R"((\w+([.-]\w+)*)@(\w+([.-]\w+)*))"}; // шаблон е-мейл адреса

smatch m;

for (string s; getline(is,s); ) // прочесть строку

if (regex_search(s, m, pat)) // ищем шаблон

addr.insert(m[0]); // сохраняем адрес в наборе

return addr;

}

Предполагаю, что вы знакомы с регулярками. Если нет – самое время ознакомиться. Заметьте, что я полагаюсь на семантику перемещений, чтобы просто и эффективно вернуть потенциально большой набор строк. Все контейнеры стандартной библиотеки обеспечивают конструкторы перемещения, поэтому нет нужды возиться с new.

Для работы примера требуется включить компоненты:

#include<string>

#include<set>

#include<iostream>

#include<sstream>

#include<regex>

using namespace std;

Проверим:

istringstream test { // инициализируем поток строкой, содержащей адреса

"asasasa\n"

"bs@foo.com\n"

"ms@foo.bar.com$aaa\n"

"ms@foo.bar.com aaa\n"

"asdf bs.ms@x\n"

"$$bs.ms@x$$goo\n"

"cft foo-bar.ff@ss-tt.vv@yy asas"

"qwert\n"

};

int main()

{

auto addr = get_addresses(test); // get the email addresses

for (auto& s : addr) // write out the addresses

cout << s << '\n';

}

Просто пример. Легко можно поменять get_addresses(), чтобы она принимала регулярку как аргумент, чтобы она могла искать URL или что угодно. Легко поменять get_addresses(), чтоб она распознавала больше одного вхождения шаблона в строке. С++ предназначен для гибкости и обобщений, но не каждая программа обязана быть фреймворком. Суть в том, что задача извлечения емейлов из потока просто выражается и просто проверяется.

6.1 Библиотеки

На любом языке писать программу только через встроенные возможности языка (if, for, и +) утомительно. И наоборот, при наличии подходящих библиотек (graphics, route planning, database) любую задачу можно выполнить, приложив разумные усилия. Стандартная ISO библиотека С++ относительно небольшая (по сравнению с коммерческими), но помимо неё есть много библиотек как с исходным кодом, так и коммерческих. К примеру, при помощи библиотек Boost, POCO, AMP, TBB, Cinder, vxWidgets, CGAL сложные вещи становятся проще. К примеру, пусть наша программка извлекает URL с веб-страницы. Для начала, мы обобщим get_addresses() для поиска любой строки, совпадающей с шаблоном.

set<string> get_strings(istream& is, regex pat)

{

set<string> res;

smatch m;

for (string s; getline(is,s); ) // прочесть строку

if (regex_search(s, m, pat))

res.insert(m[0]); // сохранить совпадение в наборе

return res;

}

Это упрощённая версия. Теперь надо как-то прочесть файл из веба. В Boost есть библиотека asio для работы с вебом:

#include <boost/asio.hpp> // подключить boost.asio

Общение с веб-сервером довольно непростое:

int main()

try {

string server = "www.stroustrup.com";

boost::asio::ip::tcp::iostream s {server,"http"}; // установить соединение

connect_to_file(s,server,"C++.html"); // проверить и открыть файл

regex pat {R"((http://)?www([./#\+-]\w*)+)"}; // URL

for (auto x : get_strings(s,pat)) // ищем ссылки

cout << x << '\n';

}

catch (std::exception& e) {

std::cout << "Exception: " << e.what() << "\n";

return 1;

}

При разборе файла www.stroustrup.com/C++.html это даёт:

www-h.eng.cam.ac.uk/help/tpl/languages/C++.html

www.accu.org

www.artima.co/cppsource

www.boost.org

Я использовал множество, поэтому URL выводятся по алфавиту.

Я спрятал проверку соединения в connect_to_file():

void connect_to_file(iostream& s, const string& server, const string& file)

// открыть соединение с сервером и открыть файл в s

// пропустить заголовки

{

if (!s)

throw runtime_error{"нет соединения\n"};

// Запросить чтение файла с сервера

s << "GET " << "http://"+server+"/"+file << " HTTP/1.0\r\n";

s << "Host: " << server << "\r\n";

s << "Accept: */*\r\n";

s << "Connection: close\r\n\r\n";

// Проверить ответ:

string http_version;

unsigned int status_code;

s >> http_version >> status_code;

string status_message;

getline(s,status_message);

if (!s || http_version.substr(0, 5) != "HTTP/")

throw runtime_error{ "недопустимый ответ \n" };

if (status_code!=200)

throw runtime_error{ "код статуса в ответе " };

// Выбросить заголовки ответа, которые заканчиваются пустой строкой:

string header;

while (getline(s,header) && header!="\r");

}

Я не писал всё с нуля. Работа с HTTP скопирована с документации по asio.

6.2 Hello, World!

С++ — компилируемый язык, предназначающийся для создания хорошего, обслуживаемого кода, для которого имеет значение быстродействие и надёжность. Он не предназначался для соревнований с интерпретируемыми скриптовыми языками, которые подходят для написания маленьких программ. JavaScript и другие подобные языки часто написаны на С++. Тем не менее, есть много полезных программ на С++, которые занимают всего несколько десятков или сотен строк.

Тут могут помочь авторы библиотек. Вместо того, чтобы концентрироваться на заумных и продвинутых вещах в библиотеках, предоставьте простые примеры “hello, world!”. Сделайте минимальную версию библиотеки, которую легко установить, и пример на одну страничку из того, что она умеет. В тот или иной момент времени мы все оказываемся в роли новичка. Кстати, вот моя версия “hello world” для С++:

#include<iostream>

int main()

{

std::cout << "Hello, World\n";

}

Более длинные и сложные версии кажутся мне менее прикольными.

7 Применения мифов

Часто у мифов есть основание. Каждому из них соответствуют моменты и ситуации, когда в них можно верить на разумном основании, основанном на доказательствах. На сегодняшний день я считаю их абсолютно ложными, простыми недоразумениями, хотя и полученными честным путём. Проблема в том, что мифы всегда служат какой-то цели, или они бы уже вымерли. Эти пять мифов служат разным целям:

— они дают комфорт. Не нужно ничего менять, переоценивать и переосмысливать. Знакомое кажется приятным. Перемены вызывают тревогу, поэтому хорошо, если новинка будет нежизнеспособной.

— можно сэкономить время. Если вам кажется, что вы знаете, что из себя представляет С++, вам не надо тратить время на изучение чего-либо нового, экспериментировать с новыми технологиями, измерять код на быстродействие, тренировать новичков.

— можно не учить С++. Если бы эти мифы были правдой, зачем его вообще нужно было бы учить?

— они помогают продвигать другие языки и технологии – в случае их правдивости это было бы необходимо.

Но они ложны, поэтому аргументы за то, чтобы сохранить всё, как есть, искать альтернативы С++ или избегать современного стиля программирования на нём, нельзя основывать на этих мифах. Существовать с устаревшим представлением о С++ в голове может и комфортно, но при работе с софтом необходимо меняться. Можно достичь большего, чем просто использовать С, С с классами, С++98 и т.д.

Приверженцы «старого, доброго» проигрывают. Затраты на поддержку часто больше, чем на написание современного кода. Старые компиляторы и инструменты обеспечивают меньшее быстродействие и проводят худший анализ, чем современные. Хорошие программисты часто отказываются от работы с антикварным кодом.

Современные версии С++ и технологии программирования, которые он поддерживает, отличаются в лучшую сторону от того представления, которое создают «общепризнанные мифы». Если вы верите в какие-то из них – не верьте мне на слово. Попробуйте, проверьте. Измерьте «старый способ» и альтернативы для актуальной проблемы. Попробуйте освоить новые методы, изучить новые возможности и технологии. Не забывайте сравнивать оценочную стоимость поддержки нового и старого способов. Лучший способ опровержения мифа – это представить доказательство. Я представил вам свои примеры и аргументы.

И я не заявляю, что С++ идеален. Он не идеален, он не является наилучшим языком для всего и для всех. Как и любой другой язык. Воспринимайте его таким, какой он сейчас, а не каким он был 20 лет назад, и не таким, как его выставляет кто-то, кто рекламирует альтернативы. Чтобы сделать рациональный выбор, поищите достоверную информацию, и попробуйте сами понять, как современный С++ справляется с вашими задачами.

8 Итог

Не верьте «общепризнанному» знанию о С++, или бездоказательному его использованию. В этой статье рассматриваются пять популярных мнений о С++ и предлагаются аргументы в пользу того, что они – всего лишь мифы:

1. Чтобы понять С++, сначала нужно выучить С

2. С++ — это объектно-ориентированный язык программирования

3. В надёжных программах необходима сборка мусора

4. Для достижения эффективности необходимо писать низкоуровневый код

5. С++ подходит только для больших и сложных программ

Эти мифы вредны.

 
 
 

Недавние посты

Смотреть все
Спецификатор constexpr в C++11 и в C++14

Одна из новых возможностей C++11 — спецификатор constexpr. С помощью него можно создавать переменные, функции и даже объекты, которые...

 
 
 

Comments


Мы в соцсетях

© 2011-2017 «Программирование. Помощь студентам».

bottom of page