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

http://www.stroustrup.com/Myths-final.pdf
  • Перевод
Часть 1

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. С++ подходит только для больших и сложных программ

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

9 Обратная связь


Остались сомнения? Сообщите мне, почему. Какие ещё мифы вы встречали? Почему они являются мифами, а не правдой? Какие у вас есть доказательства их разоблачения?

10 Ссылки


1. ISO/IEC 14882:2011 Programming Language C++
2. POCO libraries: pocoproject.org
3. Boost libraries: www.boost.org
4. AMP: C++ Accelerated Massive Parallelism. msdn.microsoft.com/en-us/library/hh265137.aspx
5. TBB: Intel Threading Building Blocks. www.threadingbuildingblocks.org
6. Cinder: A library for professional-quality creative coding. libcinder.org
7. vxWidgets: A Cross-Platform GUI Library. www.wxwidgets.org
8. Cgal — Computational Geometry Algorithms Library. www.cgal.org
9. Christopher Kohlhoff: Boost.Asio documentation. www.boost.org/doc/libs/1_55_0/doc/html/boost_asio.html
10. B. Stroustrup: Software Development for Infrastructure. Computer, vol. 45, no. 1, pp. 47-58, Jan. 2012, doi:10.1109/MC.2011.353.
11. Bjarne Stroustrup: The C++ Programming Language (4th Edition). Addison-Wesley. ISBN 978-0321563842. May 2013.
12. Bjarne Stroustrup: A Tour of C++. Addison Wesley. ISBN 978-0321958310. September 2013.
13. B. Stroustrup: Programming: Principles and Practice using C++ (2nd edition). Addison-Wesley. ISBN 978-0321992789. May 2014.

Послесловие


После публикации статьи на isocpp.org получили разные комментарии. Разрешите мне прокомментировать некоторые из них.

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

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

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

Некоторые комментаторы перешли с С++11/С++14, на которых я основывал свою аргументацию, к более старым версиям. С++14 – не С++ из 1980-х. Эти стандарты уже не такие, каким учились большинство людей. И не то, чему учатся на сегодняшних курсах. И не то, что люди видят, разглядывая довольно объёмные тексты существующих программ. Я хочу поменять это представление. Если вам не удаётся работать с моими примерами в какой-либо антикварной версии С++ или со старым компилятором – это плохо, но сегодня есть улучшенные версии всех основных компиляторов (и обычно бесплатные). В моих примерах не было никакого ультрасовременного кода.

Каждый язык программирования, достигающий успеха, сталкивается с проблемами старого кода. Не судите С++ по технологиям программирования 20-летней давности или компиляторам 10-летней давности. Взгляните на современный С++ и попробуйте воспользоваться новыми возможностями, как это уже удалось многим. Сегодня вы почти наверняка пользовались программой, написанной на С++11. Между моим и вашим компьютером очень много шагов, на которых встречаются программы на С++11.

Довольно много комментариев содержат заявления вроде «а в языке Х есть точно такая же возможность» или «библиотека Y в языке X делает именно это». Очевидно, если у вас есть язык, в котором проще, чем в С++, решить вашу задачу, при этом не теряя критично в быстродействии, переносимости и не приобретая ненужных ограничений – используйте его. Но ни один язык или библиотека не подходят идеально для всех и всего.

Я предоставил примеры для общих задачи и общих технологий. Сравнивать что-то с одним примером не особенно нужно. Моя точка зрения относится к общим вещам, а примеры – просто иллюстрации. При использовании достаточно хорошей библиотеки любой язык будет простым и приятным. Для достаточно ограниченной задачи можно сконструировать специальный язык, который будет элегантнее языка общего назначения. К примеру, библиотека asio, которую я использовал в пункте 6.1 – гибкая, эффективная сетевая библиотека общего назначения. Для любой задачи её можно обернуть в простую функцию (или небольшой набор функций), чтобы сделать её более удобной. И мой код был бы реализацией этого. То, что я пытался объяснить в п.6.2 – сообщество программистов С++ могли бы помочь программистам, проведя побольше времени над тем, чтобы делать простые вещи более простыми. Например, в 99% случаев я использую sort(v) вместо sort(v.begin(),v.end()).

Быстродействие


Мои комментарии вызвали небольшую бурю. Многие пытались опровергать их простыми возражениями. Я не принимаю аргументы по быстродействию, не подкреплённые данными о тестировании. Мои комментарии были подтверждены реальными измерениями в разных ситуациях на протяжении нескольких лет. Многие из них описаны в книгах. Они правдивы для широкого спектра схожих примеров.

Я имею в виду современную реализацию С++, соответствующую стандартам. К примеру, когда я пишу про быстродействие оптимизации коротких строк, я не имею в виду реализации С++ до С++11. Я не рассматриваю комментарии на тему, что std::sort() или std::string работают медленно без использования оптимизатора. Разумеется – но глупо обсуждать быстродействие неоптимизированного кода. При использовании GCC или Clang используйте –O2; для продуктов от Microsoft используйте release mode.

Я неплохо знаю С и его стандартную библиотеку. Я написал много кода на С ещё до того, как сегодняшние студенты родились, и многое привнёс в язык: прототипирование, константы, инлайнинг, декларации в for, декларации как объявления, и многое другое. Я следил за его развитием и эволюционированием.

Да, C-версия compose() не проверяет значение, возвращаемое malloc(). Я же спрашивал у вас, всё ли я правильно сделал. Я намеренно не предоставил вам код, годный для продакшена. Отсутствие проверки результата – один из основных источников ошибок, поэтому моя «ошибка» специально была сделана для иллюстрации этого. В данном случае часто помогают исключения. Конечно, можно было написать С-версию compose(), используя менее известные функции стандартной библиотеки, и да, можно было избежать свободного хранения, если позволить вызывающему коду передать размещённый в стеке буфер и позволить вызывающему разбираться с проблемой строковых аргументов, которые бы его переполнили. Тем не менее, эти альтернативы не относятся к главному вопросу: такой код писать сложнее, чем в С++, и ещё сложнее писать его правильно. Новички с первого раза пишут версию для С++, но не для С, особенно для тех версий, которые основаны на функциях из стандартной библиотеки, которым новичков не обучают.

С++ использовался в критических и высоконагруженных встраиваемых системах годами – в тех же марсианских Роверах (анализ обстановки и автономная работа), F-35, F-16 (системы управления полётом), и множестве других: www.stroustrup.com/applications.html. И да, космическая капсула Орион запрограммирована при помощи С++.

Библиотеки


Да, библиотеки разнятся по качеству, и иногда сложно выбрать нестандартную библиотеку из множества вариантов. Это проблема. Но эти библиотеки существуют, и их исследование часто более продуктивно, чем простое движение вперёд напролом, кончающееся изобретением очередного колеса.

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

С++ обучать легче, чем С из-за более хорошей системы типов и синтаксиса. Необходимо учить меньше трюков и костылей. Представьте, как бы вы стали учить стилю программирования на С, обучая языку С++. Я бы никогда не стал давать новичкам курс С++, который бы:

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

Хорошие учителя, преподающие С, не пытаются научить новичков всем техникам.
www.stroustrup.com/programming.html — мой ответ на вопрос «Как бы вы обучали новичков С++?». Эта система работает.

Можно ознакомиться с моей довольно старой работой по некоторым аспектам преподавания С и С++: Learning Standard C++ as a New Language. C/C++ Users Journal. pp 43-54. May 1999 (www.stroustrup.com/papers.html).

Сегодня я бы сделал С-версию курса получше, а С++ — сильно лучше. Примеры отражают стиль программирования того времени (и были рассмотрены экспертами по программированию на С и С++).

Сегодняшний С++ — это стандарт ISO С++14, а не то, что я описывал 30 лет назад, и не то, что ваш преподаватель рассказывал вам 20 лет назад. Изучите C++11/C++14 в том виде, в каком они поддерживаются основными компиляторами, и привыкните к ним. Это гораздо лучший инструмент, нежели ранние версии С++. Сегодняшний С – это стандарт ISO С11, а не K&R C (хотя я не уверен, соответствуют ли сегодняшние компиляторы стандарту С11 так же хорошо, как компиляторы С++ стандарту С++14). Меня шокируют некоторые вещи, которые сегодня преподают под видом «правильного С++».

С++ — это не ООП-язык. Это язык, поддерживающий ООП, другие техники программирования, и их комбинации. Если вы – опытный программист, я рекомендую прочесть A Tour of C++ в качестве быстрого обзора современного языка C++.
Метки:
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 62
  • –32
    #include<iostream> 
    int main() 
    { 
      std::cout << "Hello, World\n"; 
    } 
    

    Ох, и чего я на собеседованиях так много требую, даже Страуструп не знает, что эта программа не компилируется =)
    • +3
      Не компилируется или не должна?
      Потому что как минимум g++ из GCC 4.8.3 компилируется и работает.
      • +3
        Оно и будет и должно. Из того, что могло смутить незнающего человека, это отсутствие return в main() («3.6.1 Main function» в стандарте)
        • +1
          Еще до C++11 (и до сих пор в C) отсутствие newline в конце файла — UB. Возможно, специально без newline примеры, чтобы подчеркнуть, что в С++11 уже не обязательно :-)
          • +1
            Я сначала удивился, потом прочитал пункт 5.1.1.2/1.2:
            <...> A source file that is not empty shall end in a new-line character, which shall not be immediately preceded by a backslash character before any such splicing takes place.

            В любом случае, gcc это до лампочки. Он для упрощения лексера всегда дописывает \n в конец при чтении файла.
      • 0
        Скажите, в какой компании вы работаете?
        • +3
          Такие вещи легко гуглятся. За пять минут узнал все об этом персонаже. Имена, пароли, явки…
          Как же легко теперь узнать о человеке в наше время, раньше спецслужбы и мечтать не могли о таком, что доступно любому пользователю интернета сегодня. Извините за оффтоп, навеяло.
        • +4
          Очень жалко ваших кандидатов… stackoverflow.com/questions/2784075/is-a-return-statement-mandatory-for-c-functions-that-do-not-return-void

          §6.6.3/2: Flowing off the end of a function is equivalent to a return with no value; this results in undefined behavior in a value-returning function.
          §3.6.1/5. If control reaches the end of main without a return, it will have the effect of return 0;
          • –4
            Это была шутка же =)
            Вот люди злые пошли.
        • –13
          Не должна, и всю мою жизнь не компилировалась по причине отсутствия return statement.
          Upd. Упс, с телефона промахнулся. Ответ на комментарий выше.
          • +1
            Ну, значит компиляторам стало на это чхать. Проверил еще MSVC, ICC, Clang — всеми относительно поздними версиями компилируется. Если не писать дополнительных опций — как максимум выдают предупреждение.
            • –13
              Возможно, у меня по умолчанию -Wall -Werror. В любом случае, это настолько плохой стиль, насколько можно: что делать рантайму, когда он выполняет код:
              err = main();
              • +1
                Компилируется без единого варнинга c -Wall и -Wextra.
                g++ (Debian 4.9.1-19)
                • +5
                  err = main();
                  Согласно стандарту такой код запрещен: «The function main shall not be used within a program.» (3.6.1p3)

                  А программа, содержащая не-void функцию без единого return внутри, все равно обязана компилироваться (опять же согласно стандарту), но если в процессе выполнения программа доходит до конца тела такой функции (и эта функция не ::main), то это ведет к неопределенному поведению со всеми вытекающими.
                  • –3
                    Запрещен, да, но именно так написано в crt.
                    • +3
                      Таки crt — это не программа, а часть стандартной библиотеки. На её код могут не распространяться требования стандарта. Это справедливо для многих языков программирования.
              • +22
                В стандарте C++ написано что функция main может не иметь return statement. Тогда по умолчанию компилятор должен выполнить «return 0;». В пункте 3.6.1 это написано.
                • –15
                  Спасибо, не знал. Постоянно при написании примеров так же ошибаюсь и всегда получаю ошибки, видимо дело в конфиге.
                  Боже мой, везде специальные случаи, какие же плюсы стремные…
                  • +5
                    Чтобы не ошибаться в коротких примерах, когда по памяти пишешь код, советую ideone.com/
                    "… какие же плюсы стремные…" Скорее это просто незнание стандарта.
                    • –5
                      У меня на машине не компилируется.
                      • +1
                        Конфигурацию в студию.
                        • +2
                          Поддерживаю. Проверил только что на msvc 2012-2013 и на виртуалках с gcc 4.9.2. Все ок.
                    • +3
                      По-моему, Вы просто не поняли основного посыла статьи: «Многое изменилось и стало сильно проще, пробуйте!».
                  • +7
                    C++ 3.6.1:
                    A return statement in main has the effect of leaving the main function (destroying any objects with automatic storage duration) and calling std::exit with the return value as the argument. If control reaches the end of main without encountering a return statement, the effect is that of executing return 0;


                    C89 5.1.2.2.3:
                    A return from the initial call to the main function is equivalent to calling the exit function with the value returned by the main function as its argument. If the } that terminates the main function is reached, the termination status returned to the host environment is unspecified.


                    C99 5.1.2.2.3:
                    If the return type of the main function is a type compatible with int, a return from the initial call to the main function is equivalent to calling the exit function with the value returned by the main function as its argument; reaching the } that terminates the main function returns a value of 0. If the return type is not compatible with int, the termination status returned to the host environment is unspecified.


                    Всё законно и в C и в C++.
                    • 0
                      Однако же, была проблема.
                      If the } that terminates the main function is reached, the termination status returned to the host environment is unspecified.

                      Могло раньше вернуть не ноль даже если все хорошо. То есть, таки бага бы была.
                      В новых стандартах все ок, и то верно. Ну, кто не ошибается, тот ничего не делает.

                      • 0
                        Это да, наконец уточнили и внесли единообразие.
                        Точнее возвращало что придётся или в зависимости от реализации. Да и откуда компилятору знать, что всё хорошо?) Может там есть внутри блока return, а если добрались до конца, то всё плохо.
                    • 0
                      deleted
                    • +12
                      Миф 5: С++ предназначен для больших и сложных программ

                      К вопросу об опровержении Страуструпом непригодности плюсов для простых программ путём написания 60-строчной программы. За три дня народ с CodeGolf.StackExchange насобирал почти 50 языков, в которых для выполнения этой задачи нужно от 1 до 10 строк и ни одной сторонней библиотеки.

                      Debunking Stroustrup's debunking of the myth “C++ is for large, complicated, programs only”

                      Несколько оговорок:

                      1. Некоторые решения полагаются на реализацию HTTP на коленке. Они сломаются, если понадобится поддержка HTTPS, gzip, редиректов и прочих фич HTTP. Это же касается решения Страуструпа.

                      2. Обработка ошибок отсутствует почти везде. Скриптовым языкам в целом пофиг: они вывалят в консоль ошибку во всех подробностях; другие свалятся менее красиво, но пара строк ради try-catch помогут.
                      • +3
                        Проблема тут в самой «формулировке мифа». Она не четкая. Что значит «не пригоден» или «не предназначен»? Для PHP и Python есть биндинги к OpenGL. Из этого не следует, что это «лучший выбор» язык для написания компьютерных игр. Хотя, в принципе, их можно для этого использовать, и не факт, что они даже будут «худшим выбором» в данном вопросе. Но лучшем ли? Не уверен…

                        А с такой формулировкой можно находить бесконечные аргументы «за» и «против».
                        • +2
                          Я бы сформулировал миф так: «Будучи в здравом уме и трезвой памяти и зная несколько языков программирования, будет ли у меня причина выбирать C++ для простой задачи?» И теперь смотрим на пример с перечислялкой ссылок: на питоне я пишу скрипт в 1-3 строчки и имею всё в лучшем виде; на плюсах я вынужден подключать сторонние библиотеки, писать на порядок больше кода, проверять возвращённые значения и т.п. И ответ (по крайней мере для этой задачи) становится очевиден.

                          Для простых задач у языка должна быть мощная стандартная библиотека. С этим у плюсов не очень, и даже полу-стандартный буст содержит далеко не всё необходимое. Нужны немногословные выразительные средства. С этим у плюсов тоже проблемы, потому что в код лезут обёртки над указателями, константные ссылки и т.п. В результате приходится совершать больше телодвижений, чем при использовании других языков. Конечно, по производительности и другим параметрам решение на плюсах будет выигрывать, но разница может быть пренебрежимо мала (обсуждаемая задача, например, на 90% будет упираться в скорость сети).

                          С питоном и OpenGL, напротив, всё не так очевидно. Если я пишу какую-нибудь тривиальную нересурсоёмкую игру, то я не так много потеряю от реализации на тормозном языке. При этом будут очевидные плюсы: код будет проще за счёт выразительности языка, мне не нужно будет собирать бинарники и т.п. Если обратить внимание на менее тормозные языки, которые сильно проигрывают плюсам по производительности, но уже не на порядок, то можно заметить, что они уже повсеместно используются для игр.
                          • +6
                            Для простых задач у языка должна быть мощная стандартная библиотека.
                            Либо простота подключения сторонних библиотек (и публикации своих для общего доступа). У С++ с этим плохо, а у многих современных языков есть удобные удобные менеджеры пакетов, так что наличие чего-то в стандартной библиотеке становиться второстепенным.
                      • –4
                        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; 
                        } 
                        

                        Зачем так усложнять?

                        int greater(const void* p, const void* q) // трёхстороннее сравнение
                        { 
                          return *(double*)p - *(double*)q; 
                        } 
                        

                        Короче, меньше и быстрее (одна инструкция sub и ret, заместо cmp, mov и ret в x86 / amd64).
                        Так все функции сравнения в C работают (поэтому в описаниях всяких там memcmp() в возвращаемом значении и указано, что либо 0, либо >0, либо <0).

                        Мне меньше всего нравится qsort() из стандартной ISO библиотеки C:

                        А вот с этим не согласен. Если не брать в расчет варианты std::sort() где мы работает с объектами (std::pair или какие-нибудь массивы данных разного размера), то в C++ она таже самая. Т.е. мы указываем начало, конец и функцию сравнения (просто в C заместо конца мы указываем длину и размер элемента).

                        Помнится в свое время, когда в задаче требовалось использовать std::sort(), для меня было очень странным, что в аргументах передается указатель начала массива и указатель его конца.
                        • 0
                          Насчет x86 / amd64 инструкций слегка ошибся, там какие-то хитры SSE инструкции используются. Но в любом случае, в вашем варианте гораздо больше операций выполняется.
                          • +6
                            Зачем так усложнять?
                            А мы не получим ноль из-за округления там, где его не должно быть? Что будет, если разность больше максимального числа, представимого в int?

                            И вобще, double сортировать стремно, поскольку на этом множестве не определено отношение полного порядка (NaN != NaN и прочие приколы).
                            • 0
                              А ну да, насчет нуля я не подумал, каюсь.
                            • +1
                              >> Зачем так усложнять?

                              double a = (1LL сдвиг_влево 32);
                              double b = 1;
                              std::cout сдвиг_влево greater(&a, &b);

                              -2147483648
                            • 0
                              Библиотечные реализации используют один алгоритм для sort и qsort, поэтому это сравнение стилей программирования, а не алгоритмов.
                              В последнее время std::sort везде реализован на основе introsort. Внутри qsort, надо полагать, алгоритм qsort.
                              • 0
                                Не знаю как сейчас, но раньше свой QSort работал быстрее qsort'а из stdlib.h. Возможно это из-за дополнительных проверок, или в stdlib.h устойчивая сортировка реализована и т.п.
                                • +2
                                  Внутри qsort() может быть что угодно, стандарт требует только то, чтобы это был какой-то алгоритм сортировки по возрастанию.

                                  А вообще дело в том, что в случае qsort() приходится честно вызывать компаратор через указатель на функцию, которая (в принципе) может делать что угодно, поэтому компилятор перед вызовом должен всё сохранить, а после вызова — ничего не предполагать. В случае std::sort() компаратор будет встроен в тело специализации функции сортировки, что несколько развязывает руки оптимизатору.
                                  • 0
                                    Прошу прощения, я далеко не досконально знаю С++ и реализацию компилаторов оного.
                                    Но как именно происходит inlining компаратора? Если std:sort() реализован в библиотеке(бинарной) то это возможно только с помощью чего-то вроде LTO. Если реализация внутренняя компилятора(что видимо и имеет место?), то в чем упрощение inlining для компилятора C++ по сравнению с C? Если функция объявлена в том же файле, что и вызов qsort — вроде бы не так сложно заинлайнить вызов. Если нет — опять же LTO может выручить.
                                    Что именно я упускаю?
                                    • +4
                                      std::sort() — это шаблонная функция, специализированная типом итератора (и компаратора, в перегруженном варианте). Поэтому 1) компиятору известен код std::sort() в месте вызова, 2) компилятору внутри std::sort известна используемая функция-компаратор (и, возможно, даже её код).

                                      qsort() — это просто функция, которая компилируется только один раз и лежит где-то в библиотеке. Компилятору известно только что где-то есть функция, которая так называется. В месте вызова он не знает, что происходит внутри. А внутри qsort() он не знает, что делает функция из её четвёртого аргумента.

                                      В случае std::sort() оптимизацию выполняет компилятор. В случае qsort() — линкер (который тупее компилятора и имеет меньше свободы). Ну, правда, и компилятор может быть хитрым и обладать специальными знаниями о том, что qsort() делает со своим четвёртым аргументом.
                                      • 0
                                        1) Функция может быть внутренней. И по умолчанию для gcc это вроде бы так. Соответсенно компилятор про неё знает всё.
                                        2) LTO делает inlining довольно эффективно.
                                        Так что вроде бы нет причин получить код без inlining.
                                        Итого разница за счет шаблонности — код функции std:sort() известен априори компилятору C++. А для C это на усмотрение разработчика.
                                        • +2
                                          код функции std:sort() известен априори компилятору C++. А для C это на усмотрение разработчика.
                                          Это утверждение можно прочитать как: «код std::sort зашивается в компилятор, реализовать собственную сортировку разработчик не может», что мягко говоря не правда.

                                          На счет link time code generation — теоретически конечно да, но есть нюанс — любой оптимизатор штука достаточно консервативная, и проводить по собственной инициативе инлайнинг, который приведет к массивному раздуванию кода (что случится в случае оптимизации qsort) он не станет.

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

                                          Ближайший аналог шаблонов в Си — макросы.
                                          • +1
                                            Согласен. Под «априори известен» имел в виду, что исходный текст шаблонной функции std:sort() находится в заголовочном файле библиотеки и доступен для анализа и модификации, а код qsort имеет право быть целиком в бинарном файле библиотеки и в заголовочном быть только объявление. Поэтому инлайнинг для C крайне затруднён. Если эта функция была бы описана в заголовочном файле целиком и/или была макросом — разница была бы минимальной. Только за счёт явно прописанных в C++ типах.
                                          • +1
                                            qsort() — большая и сложная функция, использующая рекурсию. Поэтому компилятор не будет инлайнить вызов этой функции, а без этого он не сможет применить оптимизации, связанные с тем, что он знает значения её аргументов в compile-time (и в частности значение указателя на функцию сравнения).
                                            • +1
                                              использующая рекурсию

                                              Зависит от реализизации. В glibc, например, она нерекурсивная.
                                              • 0
                                                Да понятно, что рекурсивную версию всегда можно превратить в нерекурсивную, явно реализовав стек. Суть в том, что компилятор не станет инлайнить вызов такой большой функции.
                                                • +1
                                                  Интересно, а почему это вызов большой и сложной qsort() компилятор не будет инлайнить, а вызов такой же большой и сложной std::sort() будет?
                                                  • +2
                                                    Тоже не будет, но он заинлайнит вызовы компаратора внутри кода std::sort(), потому что для различных типов компараторов это будут различные функции. Посмотрите мое сообщение чуть ниже.
                                        • 0
                                          Если очень вкратце и на пальцах, то std::sort — это шаблон и для каждого отдельного типа компаратора он инстанцируется в отдельную функцию. Грубо говоря, при вызове std::sort(v.begin(), v.end(), std::less<int>{}) вызывается, условно, функция sort_with_std_less, а при вызове std::sort(v.begin(), v.end(), std::greater<int>{}) — другая функция, скажем, sort_with_std_greater.

                                          С другой стороны, qsort — это самая обычная, простая функция, поэтому при её вызове с любыми параметрами будет всегда выполнятся один и тот же код — тело этой функции. К слову, y std::sort начинаются абсолютно аналогичные проблемы, если вместо функционального объекта ей в качестве компаратора подсовывать указатель на функцию.
                                    • 0
                                      Сложно спорить с таким гуру, но миф 4 смотрится неоднозначно. Было бы сказано «для эффективности надо писать на C», то я бы согласился. Хотя и не понимаю, как можно сранивать инлайнер «template <class _RandomAccessIter>
                                      inline void sort(» с вызовом функции «greater». Можно было бы и в равные условия поставить. Но обобщать для низкоуровневого в целом… Реализации на SIMD/CUDA/MP работают в разы быстрее.
                                      • 0
                                        Была интересная статья от Martin Sústrik, создателя zeromq (низкоуровневая абстракция над сокетами различных видов для обмена сообщениями в разном масштабе, от ipc до внутрикластерного): Why should I have written ZeroMQ in C, not C++ (вторая часть), которая описывает проблемы использования идиом C++ в написании высокопроизводительных и относительно низкоуровневых библиотеках.

                                        Одно из этих ограничений, вылезающее в полный рост при использовании C++ на микроконтроллерах — обработка ошибок в общем и исключений в частности.
                                        • +1
                                          Статьи, при всем уважении к Мартину, крайне спорные. Обе.
                                          • 0
                                            Уровень аргументации всё же существенно выше, чем в переводимых статьях Страуструпа (чем мне они, собственно, и не нравятся). И проблема, обсуждаемая в первой части касается не только C++, но и других языков с исключениями: они неудобны для системного программирования, когда нужна предсказуемость обработки ошибок.

                                            Например, в avr-g++ код часто собирается с -fno-exceptions, т. к. и rom, и ram совсем маленькие. Неплохая статья на тему использования c++ на avr — kibergus.su/en/node/92.

                                            В случае arm-none-eabi-g++ (под bare metal arm) исключения уже иногда используют, памяти достаточно (обычно 16k+ RAM есть в наличии), но это раздувает программу и требует очень осторожного обращения, особенно в реализациях ISR.

                                            Вообще, в embedded часто хочется максимальной предсказуемости поведения кода, т. к. есть слабопредсказуемый внешний мир и железо. А время обработки исключения, как я понимаю, недетерминировано на этапе компиляции, но только на этапе линковки, т. к. полностью информация для stack unwind появляется только тогда, когда известен весь control flow.

                                            Конечно, на десктопе или arm cortex-a на этот оверхед можно забить (здесь gcc.gnu.org/onlinedocs/libstdc++/manual/using_exceptions.html его оценивают в 7% data + code size), но на микроконтроллере это уже более ощутимо. Кроме того, при раскручивании стэка попадание в C-код, собранный без -fexceptions, приведет к вызову abort(), что тоже не самое приятное поведение (например, при использовании сторонней библиотеки).

                                            По второй части его статьи (про нарушение инкапсуляции и SRP) — в конце статьи он приводит ссылку на пост от Patrick Wyatt, где описывается иной подход (на плюсах) к реализации intrusive lists.
                                            • +2
                                              Самое интересное, что в большинстве случаев все вылетаемые исключения для вызывающего являются неразличимыми, а часто нужно ошибки из разных мест обрабатывать по разному. Поэтому мне не очень понятно, чем код
                                              try {
                                                  f1();
                                              } catch (Exception) {/*обработка ошибки 1*/}
                                              try {
                                                  f2();
                                              } catch (Exception) {/*обработка ошибки 2*/}
                                              ...
                                              try {
                                                  fN();
                                              } catch (Exception) {/*обработка ошибки N*/}
                                              

                                              лучше кода
                                              status = f1();
                                              if (isError(status)) {/*обработка ошибки 1*/}
                                              status = f2();
                                              if (isError(status)) {/*обработка ошибки 2*/}
                                              ...
                                              status = fN();
                                              if (isError(status)) {/*обработка ошибки N*/}
                                              

                                              И там и там лапша, но вторая менее требовательная к компилятору/окружению.

                                              Так что, прежде чем в своем коде вы будете сообщать об ошибках через исключения, хорошо подумайте, будет ли в этих исключениях достаточно информации о том, чтобы их обрабатывать раздельно и достаточно ли публично доступных типов исключений вы завели.
                                          • +1
                                            А мне непонятен такой момент. Исключения использовать по некоторым причинам нельзя, окей. Без исключения неудобно обрабатывать ошибки в конструкторах/деструкторах, поэтому будем использовать вместо них ручные функции создания и удаления. Допустим. Но в чем причина отказываться от С++ в пользу С, ведь код в таком стиле можно и на С++ написать (в конце концов, код на С с минимальными изменениями — это валидный код на С++)? В С++ есть разные другие приятные плюшки: шаблоны, пространства имен, перегрузка функций и аргументы по умолчанию, возможность создания функций-членов у структур, ссылки и т.д. При переходе мы все это потеряем и вроде как ничего не приобретем взамен, в чем смысл тогда?
                                            • 0
                                              Если преимуществ C++ почти не даёт (разве что шаблоны, которые, вроде, в zmq и не использовались), то зачем заставлять пользователей тащить плюсовый рантайм? Зачем усложнять создание биндингов для других языков (java, ruby, python, lua, haskell, etc)?

                                              Пространства имён на C реализуются консистентным использованием префиксов (в nanomsg — nn_). Остальное для низкоуровневой библиотеки (напомню, единица передачи у которой — массив байт) — не нужно. Это библиотека предоставляет api уровня bsd socket, т. е. ниже только реальный socker api и syscall'ы.

                                              Т. е. для zmq исходный выбор C++ был не очень. Код там и так C-like, просто общее состояние инкапсулировалось в специальном объекте. Точно так же можно таскать это состояние в структуре, как обычно делают в C и передавать первым параметром, отказавшись от синтаксического сахара.

                                              В итоге получается компактная библиотека, применимая, в том числе, в embedded.
                                          • –1
                                            4.3. Типобезопасность.
                                            ЧТО???

                                            Каким боком delete к типобезопасности? Тема не раскрыта.
                                            Имеете в виду, что у T* есть delete, а у shared_ptr — нет?
                                            Ну так и без delete ровно то же самое делается.
                                            shared_ptr<int> p (new int(123));
                                            ++ *p;
                                            p.reset();
                                            ++ *p; // разыменование нуль-указателя
                                            


                                            Типобезопасно было бы, если бы сделать что-то аналогичное фантомным типам в хаскелле и скале.
                                            Новое состояние — новый тип, со своим комплектом возможностей и ограничений.

                                            Хотя, конечно, отследить валидность указателя-копии — конкретного экземпляра! а не типа вообще — на weak_ptr легко.
                                            shared_ptr<int> p(new int(123));
                                            weak_ptr<int> q(p);
                                            p.reset();
                                            
                                            if(shared_ptr<int> r = q.lock())
                                                ++*r;
                                            else
                                              cout << "сдохло";
                                            
                                            • +2
                                              Вообще-то, контрпример должен быть другим, ровно то, что написано в примере из статьи, но без delete и с shared_ptr:
                                              shared_ptr<int> p (new int(123));
                                              shared_ptr<int> q = p;
                                              ++ *p;
                                              p.reset();
                                              ++ *q; // разыменование валидного указателя
                                              

                                              Именно от таких проблем он и спасает.
                                              • 0
                                                Э, нет. Это уже другая семантика. «Держаться до последнего».

                                                Конечно, неизвестно, какую семантику хотел автор статьи реализовать.
                                                То ли ему нужно было убить объект и гарантировать, что никто не покусится на труп.
                                                То ли погрузить в забытье (ну и что, что кто-то ещё ковыряется с уже ненужным объектом: поковыряется и тоже забудет).
                                                То ли наоборот, не убивать ни в коем случае, но избежать при этом утечек памяти. (Хотя некоторые утечки всё-таки сделать можно).

                                                Это показывает, что автоматизация управления памятью (смартпоинтеры одним способом, сборка мусора другим способом) — низкоуровневый инструмент. Да, просто взяли и избавились от стрельбы по памяти (кроме стрельбы по нулю). А продумывать логику за программиста смартпоинтеры и сборщики мусора не станут.
                                              • 0
                                                Имелось ввиду, что смартпоинтеры спасают от проблем, связанных с ручным управлением памяти: нельзя забыть удалить объект или, наоборот, удалить его несколько раз, а также нельзя обратиться к уже удаленному объекту. А то, о чем вы написали, это возможность иметь нулевой указатель. Да, такая возможность в общем случае требует проверять указатель на 0 перед разыменованием, но без неё было бы очень неудобно. Альтернатива: кидать исключение при попытке разыменования, но это вносило бы дополнительный оверхед в те места, где такая проверка не нужна, впрочем такой функционал легко реализовать самому.
                                                • +1
                                                  Так это называется просто безопасность, а не типобезопасность.
                                                  Типобезопасность — это недопущение приведений типов (особенно, реинтерпрет-каста).

                                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.