Pull to refresh

Comments 62

#include<iostream> 
int main() 
{ 
  std::cout << "Hello, World\n"; 
} 

Ох, и чего я на собеседованиях так много требую, даже Страуструп не знает, что эта программа не компилируется =)
Не компилируется или не должна?
Потому что как минимум g++ из GCC 4.8.3 компилируется и работает.
Оно и будет и должно. Из того, что могло смутить незнающего человека, это отсутствие return в main() («3.6.1 Main function» в стандарте)
Еще до C++11 (и до сих пор в C) отсутствие newline в конце файла — UB. Возможно, специально без newline примеры, чтобы подчеркнуть, что в С++11 уже не обязательно :-)
Я сначала удивился, потом прочитал пункт 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 в конец при чтении файла.
Скажите, в какой компании вы работаете?
Такие вещи легко гуглятся. За пять минут узнал все об этом персонаже. Имена, пароли, явки…
Как же легко теперь узнать о человеке в наше время, раньше спецслужбы и мечтать не могли о таком, что доступно любому пользователю интернета сегодня. Извините за оффтоп, навеяло.
Очень жалко ваших кандидатов… 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;
Это была шутка же =)
Вот люди злые пошли.
Не должна, и всю мою жизнь не компилировалась по причине отсутствия return statement.
Upd. Упс, с телефона промахнулся. Ответ на комментарий выше.
Ну, значит компиляторам стало на это чхать. Проверил еще MSVC, ICC, Clang — всеми относительно поздними версиями компилируется. Если не писать дополнительных опций — как максимум выдают предупреждение.
Возможно, у меня по умолчанию -Wall -Werror. В любом случае, это настолько плохой стиль, насколько можно: что делать рантайму, когда он выполняет код:
err = main();
Компилируется без единого варнинга c -Wall и -Wextra.
g++ (Debian 4.9.1-19)
err = main();
Согласно стандарту такой код запрещен: «The function main shall not be used within a program.» (3.6.1p3)

А программа, содержащая не-void функцию без единого return внутри, все равно обязана компилироваться (опять же согласно стандарту), но если в процессе выполнения программа доходит до конца тела такой функции (и эта функция не ::main), то это ведет к неопределенному поведению со всеми вытекающими.
Запрещен, да, но именно так написано в crt.
Таки crt — это не программа, а часть стандартной библиотеки. На её код могут не распространяться требования стандарта. Это справедливо для многих языков программирования.
В стандарте C++ написано что функция main может не иметь return statement. Тогда по умолчанию компилятор должен выполнить «return 0;». В пункте 3.6.1 это написано.
Спасибо, не знал. Постоянно при написании примеров так же ошибаюсь и всегда получаю ошибки, видимо дело в конфиге.
Боже мой, везде специальные случаи, какие же плюсы стремные…
Чтобы не ошибаться в коротких примерах, когда по памяти пишешь код, советую ideone.com/
"… какие же плюсы стремные…" Скорее это просто незнание стандарта.
У меня на машине не компилируется.
Поддерживаю. Проверил только что на msvc 2012-2013 и на виртуалках с gcc 4.9.2. Все ок.
По-моему, Вы просто не поняли основного посыла статьи: «Многое изменилось и стало сильно проще, пробуйте!».
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++.
Однако же, была проблема.
If the } that terminates the main function is reached, the termination status returned to the host environment is unspecified.

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

Это да, наконец уточнили и внесли единообразие.
Точнее возвращало что придётся или в зависимости от реализации. Да и откуда компилятору знать, что всё хорошо?) Может там есть внутри блока return, а если добрались до конца, то всё плохо.
Миф 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 помогут.
Проблема тут в самой «формулировке мифа». Она не четкая. Что значит «не пригоден» или «не предназначен»? Для PHP и Python есть биндинги к OpenGL. Из этого не следует, что это «лучший выбор» язык для написания компьютерных игр. Хотя, в принципе, их можно для этого использовать, и не факт, что они даже будут «худшим выбором» в данном вопросе. Но лучшем ли? Не уверен…

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

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

С питоном и OpenGL, напротив, всё не так очевидно. Если я пишу какую-нибудь тривиальную нересурсоёмкую игру, то я не так много потеряю от реализации на тормозном языке. При этом будут очевидные плюсы: код будет проще за счёт выразительности языка, мне не нужно будет собирать бинарники и т.п. Если обратить внимание на менее тормозные языки, которые сильно проигрывают плюсам по производительности, но уже не на порядок, то можно заметить, что они уже повсеместно используются для игр.
UFO just landed and posted this here
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(), для меня было очень странным, что в аргументах передается указатель начала массива и указатель его конца.
Насчет x86 / amd64 инструкций слегка ошибся, там какие-то хитры SSE инструкции используются. Но в любом случае, в вашем варианте гораздо больше операций выполняется.
Зачем так усложнять?
А мы не получим ноль из-за округления там, где его не должно быть? Что будет, если разность больше максимального числа, представимого в int?

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

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

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

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

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

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

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

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

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

Одно из этих ограничений, вылезающее в полный рост при использовании C++ на микроконтроллерах — обработка ошибок в общем и исключений в частности.
Статьи, при всем уважении к Мартину, крайне спорные. Обе.
Уровень аргументации всё же существенно выше, чем в переводимых статьях Страуструпа (чем мне они, собственно, и не нравятся). И проблема, обсуждаемая в первой части касается не только 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.
Самое интересное, что в большинстве случаев все вылетаемые исключения для вызывающего являются неразличимыми, а часто нужно ошибки из разных мест обрабатывать по разному. Поэтому мне не очень понятно, чем код
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*/}

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

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

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

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

В итоге получается компактная библиотека, применимая, в том числе, в embedded.
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 << "сдохло";
Вообще-то, контрпример должен быть другим, ровно то, что написано в примере из статьи, но без delete и с shared_ptr:
shared_ptr<int> p (new int(123));
shared_ptr<int> q = p;
++ *p;
p.reset();
++ *q; // разыменование валидного указателя

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

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

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

Articles