Готовы погрузиться с головой в дивный мир программирования? Хотите узнать как непредсказуемо могут повести себя несколько простых строк кода?
Если ваш ответ "Да!" — добро пожаловать под кат.
Вас будут ждать несколько занимательных задачек на С или С++.
Правильный ответ с объяснением всегда будет спрятан под спойлером.
Удачи!
Про самую короткую программу
main;
Что будет если скомпилировать эту программу компилятором языка C?
- Не cкомпилируется.
- Не слинкуется.
- Скомпилируется и слинкуется.
Это валидный код на языке C.
Почему? В C можно опустить тип возвращаемого значения у функций и при объявлении переменных, по-умолчанию он будет int-ом. А ещё в С нет различия между функциями и глобальными переменными при линковке. В этом случае линковщик думает что под именем main находится функция.
Про fork
#include <iostream>
#include <unistd.h>
int main() {
for(auto i = 0; i < 1000; i++)
std::cout << "Hello world!\n";
fork();
}
Сколько раз будет напечатано "Hello world!"?
- 1000
- меньше
- больше
IO операции буферизуется для улучшения производительности.
Вызов fork()
породит новый процесс, с copy-on-write дубликатом адресного пространства.
Буферизованные строчки будут напечатаны в каждом процессе.
Про индексы
#include <iostream>
int main() {
int array[] = { 1, 2, 3 };
std::cout << (4, (1, 2)[array]) << std::endl;
}
Что напечатет этот код?
- 1
- 2
- 3
- 4
- Ошибка компиляции
- Не определено стандартом.
Порграмма напечатает 3.
Почему так?
Сначала посмотрим на индекс: array[index] == *(array + index) == *(index + array) == index[array]
Дальше мы имеем дело с бинарным оператором запятая. Он отбрасывает свой левый аргумент и возвращает значение правого.
Про регулярное выражение
#include <regex>
#include <iostream>
int main() {
std::regex re { "(.*|.*)*O" };
std::string str { "0123456789" };
std::cout << std::regex_match(str, re);
return 0;
}
За какое минимальное время точно заматчится эта регулярка?
- ~ 1 мс.
- ~ 100 мс.
- ~ 1 cек.
- ~ 1 мин.
- ~ 1 час.
- ~ 1 год.
- больше времени жизни вселенной.
Ха-ха! Вот и не угадали. Зависит от компилятора.
На моём ноутбуке clang показывает результат примерно 100 мс.
GCC 57 секунд! Минута! Серьёзно?!
Почему так?
Есть 2 подхода для реализации регулярных выражений.
Один — превратить регулярное выражение в конечный автомат за O(n**2)
, для регулярного выражения длиной n символов.
Сложность сопоставления co строкой из m символов — O(m)
. Такое регулярное выражение не поддерживает backtracking.
Второй — что-то вроде жадного перебора с поиском в глубину. Поддерживает backtracking.
А ещё, сложность операций с регулярными выражениями в STL никак не определена. Хорошо хоть, что за минуту управились.
Про move и лямбду
#include <iostream>
struct Foo {
Foo() { std::cout << "Foo()\n"; }
Foo(Foo&&) { std::cout << "Foo(Foo&&)\n"; }
Foo(const Foo&) { std::cout << "Foo(const Foo&)\n"; }
};
int main() {
Foo f;
auto a = [f = std::move(f)]() {
return std::move(f);
};
Foo f2(a());
return 0;
}
Какую строчку программа напечатает последней?
Foo()
Foo(Foo&&)
Foo(const Foo&)
Foo(const Foo&)
. По умолчанию лямбды иммутабельны. Ко всем значениям указанным в []
неявно добавляется const
.
Это позволяет лямбдам вести себя как обычным функциям. При одних и тех же аргументах возвращать одни и те же значения.
Что же происходит в этом случае? Когда мы пытаемся сделать move f
из функции, у нас получается const Foo&&
.
Это очень странная штука, компилятор не умеет с ней работать и копирует Foo
. Можно починить объявив mutable лямбду:
auto a = [f = std::move(f)]() mutable {
return std::move(f);
};
Или сделать конструктор от Foo(const Foo&&)
.
Про x и bar
#include <iostream>
int x = 0;
int bar(int(x));
int main() {
std::cout << bar;
}
Что произойдёт если попытаться скомпилирвать и запустить это?
- напечатает
0
- напечатает
1
- напечатает
0x0
- не скомпилируется
- не слинкуется
Программа напечатает 1
.
Почему так?
int bar(int(x));
— это объявление функции, оно эквивалентно int bar(int x);
.
Если вы хотите приведение типа, надо писать вот так int bar((int(x)));
.
Затем мы пытаемся вывести адрес функции, он будет неявно приведён к bool, адрес функции не может быть нулём, т.е. true
.
Функция bar()
не используется. Поэтому при линковке не будет unreferenced symbol.
Про inline
#include <iostream>
inline size_t factorial(size_t n) {
if (n == 0)
return 1;
return n * factorial(n - 1);
}
int main() {
std::cout << factorial(5) << std::endl;
}
Программа компилируется и линкуется без ошибок вот так g++ -c main.cpp -o main.o && g++ foo.cpp -o foo.o && g++ foo.o main.o -o test
. Что произойдёт если ее запустить?
- Напечатается 120.
- Может произойти что угодно.
Может произойти что угодно. Это же С++.
Весь подвох в слове inline. Это лишь указание компилятору.
Он может просто вкомпилировать эту функцию в объектный файл (скорее всего, он так и сделает для рекурсивных функций).
Линковщик умеет выкидывать дубликаты не встроенных в код inline-функций.
В итоговый файл обычно попадает тот вариант, который встретился в первом объектном файле.
Программа выведет 0
если в foo.cpp:
#include <cstddef>
inline size_t factorial(size_t n) {
if (n == 0) return 0;
return 2 * n * factorial(n - 1);
}
int foo(size_t n) {
return factorial(n);
}
Про конструкторы
#include <iostream>
struct Foo {
Foo() { std::cout << "Foo()\n"; }
Foo(const Foo&) { std::cout << "Foo(const Foo&)\n"; }
Foo(int) { std::cout << "Foo(int)\n"; }
Foo(int, int) { std::cout << "Foo(int, int)\n"; }
Foo(const Foo&, int) { std::cout << "Foo(const Foo&, int)\n"; }
Foo(int, const Foo&) { std::cout << "Foo(int, const Foo&)\n"; }
};
void f(Foo) {}
struct Bar {
int i, j;
Bar() {
f(Foo(i, j));
f(Foo(i));
Foo(i, j);
Foo(i);
Foo(i, j);
}
};
int main() { Bar(); }
Какая строчка будет напечатана последенй?
Foo(int, int)
Foo(const Foo&, int)
Foo(int, const Foo&)
Foo(int)
Последней строчкой будет Foo(const Foo&, int)
.
Foo(i)
— объявелние переменной, оно эквивалентно Foo i
, а значит поле класса i
пропадёт из области видимости.
Заключение
Надеюсь, вы никогда не увидите это в реальном коде.
Комментарии (69)
prika148
09.06.2018 11:29У меня вопрос про «x и bar».
Насколько я понял, компилятор оптимизирует оператор приведения к bool, просто подставляя true вместо bar, верно? Если бы этой оптимизации не было, а выполнение операции (bool) bar осталось для рантайма, тогда бы линковщик не смог слинковать программу (как если бы, например, мы написали std::cout << (void*)bar).
Почему компилятор может утверждать, что адрес bar не равен нулю?mayorovp
09.06.2018 12:05Потому что адрес функции не может быть нулевым указателем в принципе.
shaggyboo Автор
10.06.2018 01:31+1Интересно, почему
<<
пытается приводить адрес функции кbool
, а не пытается его вывести как есть?deviant_9
10.06.2018 16:55Потому что operator<< перегружен для const void* (к которому приводятся любые объектные типы), но не для указателей на функции (кроме функций с сигнатурами манипуляторов вроде endl или flush). Выводить адреса объектов полезнее, чем адреса функций.
humbug
10.06.2018 18:31Вообще-то адрес функции может быть нулевым. Надо об этом помнить, чтобы не попасть в описанную ситуацию: https://www.reddit.com/r/cpp/comments/6xeqr3/compiler_undefined_behavior_calls_nevercalled/
Читаем:
- http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
- http://blog.llvm.org/2011/05/what-every-c-programmer-should-know_14.html
- http://blog.llvm.org/2011/05/what-every-c-programmer-should-know_21.html
И переходите на Rust.
vamireh
10.06.2018 18:45+1Вообще-то адрес функции может быть нулевым
Нет, адрес функции не может равен null pointer. По стандарту. Смотрите обсуждение ниже.
И переходите на Rust.
Да, слава Богу, что есть Rust — туда наконец-то свалят все, кто не понимает разницы между 0 и null pointer. Поскорее бы!humbug
10.06.2018 19:32-1Молодой человек, в данном треде обсуждают возможность адреса функции принимать значение нуля, то есть 0. Что и было доказано прувом. Хотите больше? Получите.
vamireh
10.06.2018 19:38+1Вы, старый человек, не понимаете разницы между «адресом функции» и «указателем на функцию». Вам действительно лучше писать на Rust… Или нет — на Python.
Возьмите адрес существующей функции, потом сравните его его с null pointer, получите в результате этого сравнения true и… тогда вы узнаете, что используете не соответствующий Стандарту компилятор :)humbug
10.06.2018 19:44Возьмите адрес существующей функции, потом сравните его его с null pointer, получите в результате этого сравнения true и… тогда вы узнаете, что используете не соответствующий Стандарту компилятор :)
gcc и clang с вами не согласны.
humbug@home:~$ cat test.cpp #include <cstdlib> #include <iostream> typedef int (*Function)(); static Function Do; int main() { std::cout << (Do == nullptr) << std::endl; return 0; } humbug@home:~$ g++ test.cpp humbug@home:~$ ./a.out 1 humbug@home:~$ clang++ -std=c++11 test.cpp humbug@home:~$ ./a.out 1
vamireh
10.06.2018 19:47+3Я смотрю вы всё так же не хотите в упор видеть разницу между адресом функции и указателем на функцию :D Неужели вам кто-то за программирование зарплату платит?
deviant_9
10.06.2018 20:41+2Вас просили взять «адрес существующей функции». Функция у вас здесь одна — это main, и адрес вы её не берёте (да и нельзя по Стандарту). Вы вообще никакой адрес здесь не берёте, а просто сравниваете value-initialized переменную типа «указатель на функцию» с null pointer value.
Что-то уж совсем толсто.deviant_9
10.06.2018 20:48+1Пардон — не value-initialized, а zero-initialized. Но для переменной типа «указатель на функцию» это одно и то же.
deviant_9
10.06.2018 20:30+2Во-первых, я здесь приводил прямую цитату из Стандарта C++. Не может.
Во-вторых, по поводу ваших ссылок. Если объявить переменную типа «указатель на функцию», то она (а не адрес какой-либо функции!), конечно, может иметь значение null pointer value.
Только вот в этом случае она ни на какую функцию не указывает и потому вызывать её нельзя. Более того, последнее прямым текстом сказано по одной из ваших же ссылок:
>… calling a null pointer is undefined
Представим на секунду, что у некой функции нулевой адрес (в смысле, её адрес — null pointer value). Возьмём адрес этой функции. Попробуем её вызвать через получившийся указатель… И получим неопределённое поведение. Не находите это странным? Ваша ссылка не подтверждает, а опровергает ваш тезис.
В описанной на reddit-е ситуации в программе неопределённое поведение. Неопределённое поведение может проявляться _как_угодно_, это в принципе ничего не может говорить о языке.
В данном случае компилятор видит, что указатель Do может иметь лишь два возможных значения: _либо_ null pointer value, _либо_ адрес функции EraseAll. Вызов null pointer value — неопределённое поведение, поэтому компилятор, видя вызов через указатель Do, предполагает (имеет полное право, по самому определению UB), что значением указателя является всё-таки адрес функции EraseAll — и в качестве оптимизацию производит замену вызова по указателю на вызов непосредственно EraseAll — при том, что на самом деле указатель Do всегда имеет значение null pointer value.
Это не говорит о том, что адрес функции EraseAll есть null pointer value. Это просто пример того, как компилятор честно пришёл к _ложному_ выводу по той причине, что программист нарушил контракт со своей стороны (допустил неопределённое поведение — в данном случае вызов через нулевой указатель).
Mirn
09.06.2018 12:31В некоторых архитектурах код можно и даже нужно класть в регион начинающийся с нулевого адреса.
Что нарушает соглашение что 0 это не валидный указатель и многие библиотеки могут не заработать в зависимости от фаз луны (по факту куда попала одна несчастная функция-первенец).
Пример такого: STM32F7xx (а это очень актуальная для IoT платформа) — там есть специальная память для кода начинающаяся с нулевого адреса специально для целей оптимизации: в архитектуре Thumb2 мало-битные константы можно загружать из кода команды, но обычные 32 битные адреса не подходят и они кладутся после тела функции, это приводит к тому что добавляются несколько доп инструкций на константу, чтоб сделать заметно быстрее код особенно для целевого применения (обработка звука и видео) специально сделана именно такая память именно для кода именно с нулевого адреса. Более того она подключена напрямую к алу процессора и только к нему, без арбитров и тд. Т.е. быстрее даже кеша.
Это даёт: я измерял на примере одного и того же кода радио модема у STM32F7 примерно 100 инструкций на семпл, у Cortex A54 (малинка) 250 инструкций на семпл, у STM32F4 — почти тоже ядро, почти тот же набор инструкции но в обычном озу с 0x20000000 300-400 инструкций на семпл. И даже быстрее Core i3. Что не удивительно т.к. на FIR фильтры и прочее ДСП требуется грузить в регистры до несколько десятков констант на выборку. Как правило в 2 раза больше констант требуется чем умножений с накоплениями.
Другой пример: находить в реалтайме с камеры треугольник и проконтролировать чтоб никто в его периметр руки не сувал — под поток плазмы (эксперементальное п/п производство). Вне этой памяти F7 даже с 640х480 не справляется (менее 5 фпс по тз), когда кладёшь в эту памяти то 7-8 фпс можно получить даже на 800х600.
В обоих случаях я заметил что компилятор специально кладёт в первое слово nop а тело функции начинается с второго слова, т.е. адрес становится 0х00000002.deviant_9
09.06.2018 15:02Что нарушает соглашение что 0 это не валидный указатель
Это требование Стандарта, а не просто «соглашение».
A null pointer constant can be converted to a pointer type; the result is the null pointer value of that type and is distinguishable from every other value of object pointer or function pointer type.
(C++17 7.11 Pointer conversions [conv.ptr];
C++11/14 4.10 Pointer conversions [conv.ptr])
A null pointer constant can be converted to a pointer type; the result is the null pointer value of that type and is distinguishable from every other value of pointer to object or pointer to function type.
(C++98/03 4.10 Pointer conversions [conv.ptr])vamireh
10.06.2018 01:31+2Как вы можете заметить, Стандарт нигде не говорит, что битовое представление null pointer value будет состоять из всех нулей. Он лишь говорит, что написав
T *p = 0;
мы получим в p некое значение, которое отличается от всех возможных значение указателей. Для платформы, где по адресу, битовое представление которого состоих из одних нулей, возможно размещение кода и/или данных, компилятор может использовать для null pointer value какое-нибудь другое битовое представление.deviant_9
10.06.2018 02:30Верно. Правда, учитывая количество любителей занулять структуры memset-ом (и соображения совместимости C++ с C) и просто неудобство подхода, на практике разработчики компиляторов на такое никогда не пойдут)
В любом случае преобразование адреса функции к bool гарантированно возвращает true, так как оно опирается именно на null pointer value.vamireh
10.06.2018 18:42Правда, учитывая количество любителей занулять структуры memset-ом
Любители конструкций с потерей типа должны страдать. По определению.
и соображения совместимости C++ с C
А в C null pointer обязательно имеет именно нулевое битовое представление?
В любом случае преобразование адреса функции к bool гарантированно возвращает true, так как оно опирается именно на null pointer value.
Да я и не возражаю.deviant_9
10.06.2018 21:05А в C null pointer обязательно имеет именно нулевое битовое представление?
Нет, конечно. Я имею в виду, что использование memset для зануления всех полей структуры (равно как и calloc для выделения памяти под структуру с изначальным её занулением) — распространённый приём в Си, поэтому, если последовательность из нулевых байтов не будет хотя бы одним из возможных object representation для null pointer value, то сломается куча кода.
А из соображений совместимости Си и C++ у этих языков должны быть одинаковые object representation для null pointer value.
0xd34df00d
09.06.2018 18:00+1Битовое представление нулевого указателя не обязано быть равным битовому представлению целочисленного нуля.
F376
10.06.2018 01:32Здесь таится ловушка. Дело в том, что конверсия «null pointer constant» в указатель в стандарте оговорена отдельно, при этом для финального значения конверсии (т.е. значения указателя) не гарантируется битовое значение «все нули». Вместо этого указывается что это implementation-defined.
Что же касается «Про x и bar»
#include <iostream> int x = 0; int bar(int(x)); int main() { std::cout << bar; }
Не все так просто, как это написал автор. Это «хак» Weak symbol GCC и ELF, стандарт его (имхо) не оговаривает. С компилятором Visual C++ дело окончится ошибкой линкера «unresolved external symbol».
shekelgruber
10.06.2018 01:31Потому что это адрес функции, он «по определению» не может быть нулевым (можно покопаться в стандарте — но как минимум это так в подавляющем большинстве реализаций).
creat0r
09.06.2018 12:13Я правильно понимаю, что по fork правильный ответ «1000 или больше»?
Из-за разных настроек буферизации на момент вызова fork() заведомо неизвестно, сколькостроксимволов уже отправлено на вывод, а сколько ещё ждёт очереди.
А если перед fork() написать std::cout << std::flush, то всегда будет 1000.
Gryphon88
09.06.2018 12:15Когда на практике используется оператор запятая, кроме циклов и объявления переменных?
hdfan2
09.06.2018 13:01Из всех вариантов единственный, за который не надо бить ногами, это использование в 3-й части for. И да, запятая в объявлении переменных, это не оператор «запятая», а несколько иная сущность.
deviant_9
09.06.2018 14:35В C++17 fold-expression с операцией следования позволяет без рекурсии совершить произвольное действие над всеми элементами parameter pack-а.
Простой (но плохой) пример:
#include <utility> #include <iostream> template<typename T> void do_smth(T&& arg) { std::cout << arg << '\n'; } template<typename... Types> void do_smth_with_all(Types&&... args) { (..., do_smth(std::forward<Types>(args))); } int main() { do_smth_with_all(9, 1.3, "string"); }
Мне пригодилась в игрушечном проекте при расчёте вклада отдельной наблюдаемой (зависящей от известного на этапе компиляции числа модельных переменных, разного для наблюдаемых разных видов) в общий градиент ошибки — выглядело это так:
Здесь ArgsIndexes — параметр шаблона, объявлен как(..., (gradient[asIndex(observable.vars_[ArgsIndices])] += residual * std::get<ArgsIndices>(observable_gradient)));
std::size_t... ArgsIndices
QtRoS
09.06.2018 23:39Почитайте исходники Qt. Там до появления в стандарте (и в реализациях) шаблонов с переменных числом аргументов очень много магии было сделано с хитростями, в том числе с оператором запятая для удобства. Если мне не изменяет память, то где-то в передаче аргументов «функции».
Arris
09.06.2018 13:18Надеюсь, я никогда не увижу эти головоломки на собеседовании.
Mirn
09.06.2018 15:28всё гораздо хуже, сейчас модны крайне идиотские задачки вида:
1. дан массив в миллиард float с значением 1.0 кроме одного равного (1.0 плюс или минус машинный эпсилон) в произвольном месте массива.
нужно отсортировать максимально быстро при этом ОЗУ на сортировку нет вообще, даже восьми байт.
2. дана локальная статик константная переменная функции А модуле В, надо её изменить из модуля С при этом В и С друг друга не включают через инклудники, неявно и косвенно физический адрес передавать нельзя как самой переменной так и функции. Пользоваться аппратными средствами типа ДМА нельзя и даже дублированиями вирт таблиц CPU так же нельзя. Назвать минимум три принципиально разных варианта. У вас минута на обдумывание.
Вот такие вот были недавно.
Вакансия — учёный исследователь в области распознавания изображений.Mirn
09.06.2018 15:35извиняюсь забыл уточнить про пункт 1: человек очень хотел поговорить про О больше алгоритмов сортировки и категорически не принимал ответ что построить гистограмму, просто пройтись до первого неповторяющегося и положить его в начало-конец и даже то что в реальных задача всегда есть шум и такая мелкая погрешность не важна — игнорировались, ему надо было потеоретезировать про сами алгоритмы.
Arris
09.06.2018 16:32Да уж.
Вторая задача вообще имеет решение? :)
Или там тоже теоретик нашёлся? ;)Mirn
09.06.2018 17:22нередко задачки идут либо без решения — когда собеседующий что либо хотел сделать но ответа правильного не знает, либо заведомо неправильные чтоб проверить ход мыслей/знание стандартов.
Но в этой задаче как и во второй он хотел получить один единственный правильный ответ, пресекал попытки порассуждать в слух, расспросить больше подробностей и даже ответ «я не знаю» или перво в голову пришедшее с скоростью log2(N) типа «ну например битонная сортировка» раз ему надо «самое быстрое» — значит самодельный чип на сортировочных сетях. Вообще самое тяжолое собеседование было — что хочет не понятно, побеседовать нельзя, время оговорено а истинные параметры ответа фиксированы но не сообщаются. Так же не принимался ответ: «ну тогда не знаю», и просто промолчать оговоренный ранее таймаут. И вроде разраб а не HR, под конец ещё обиделся и проскочило устало-раздосадованное "… здец!".
телепат наверное нужен был.
sergio_nsk
09.06.2018 13:41Он отбрасывает свой левый аргумент и возвращает значение правого.
Это написано не точно для оператора запятая. Он ничего не отбрасывает. Просто возвращает результат своего последнего операнда. Отбрасывать свои правые, но не левые операнды могут только логические выражения.
powerman
09.06.2018 23:14Есть 2 подхода для реализации регулярных выражений.
Почитайте https://github.com/google/re2/wiki/WhyRE2 и тамошние ссылки. RE2 поддерживает далеко не все фичи PCRE, но большинство, и при этом работает за линейное время от длины входных данных.
2energycell
10.06.2018 01:15for(auto i = 0; i < 1000; i++)
std::cout << "Hello world!\n";
fork();
не понял ответа… так 1000, больше или меньше?
пс: мой ответ: пока дойдет до форка, фор-луп уже закончит выполнение — поэтому 1000shaggyboo Автор
10.06.2018 01:28А вот и нет.
std::cout
буфирезует вывод. Данные кэшируются в памяти и будут распечатаны сразу пачкой. Размер кэша зависит от библиотеки. Послеfork()
закэшированные данные напечатаются 2 раза.
Нужно явно сбрасывать буфер черезendl
илиflush
.
Например, на Mac OS у меня выводилось ровно 2000 строчек, на Linux около 1050.2energycell
10.06.2018 10:48я понимаю что вывод на печать — он асинхронный, также понимаю про shared/duplicated resources. Но команда вывода на печать вызывается то только из первого процесса. А во втором — ну ок, есть продублированный кеш, но никто не запрашивал вывод печати.
Я тоже провел тест на Маке, даже задал 100,000 строчек — ровно столько же и вышло.
П.С — на с++ никогда не писал, но код (концепт) он и в африке код…mayorovp
10.06.2018 11:34flush всегда автоматически делается после окончания работы программы. И делается он почему-то в обоих процессах, а не только в одном :-) А почему у вас на Маке вышло ровно 100,000 строк — da-nie объяснил выше.
2energycell
10.06.2018 11:57без \n на тестах выходит тоже самое, и вообще почему почему endl должен влиять на флоу программы?
2energycell
10.06.2018 12:24и еще что-то — я сначала подумал что вывод на печать он асинхронный, но похоже я ошибся и он синхронный, и при таком раскладе вообще не понятно, как может напечататься больше 1000…
deviant_9
10.06.2018 16:22Не асинхронный, а буферизованный. flush дёргается при завершении программы из деструктора глобального объекта класса std::ios_base::Init: en.cppreference.com/w/cpp/io/ios_base/Init
У меня в Федоре 1123 строки получается. (std::endl, в отличие от просто '\n', если что, делает flush — может быть вы его написали?)
sshmakov
10.06.2018 01:45То, что в регулярке последний символ буква "О", а не цифра "0" наводит на мысль, что не сматчиться.
ganqqwerty
10.06.2018 03:53+1После первой задачки и попыток представить, где мне это пригодится, вспомнился неотвеченный вопрос. Я в начале своей карьеры четыре года писал на плюсах и так и не смог понять культуру сообщества.
Почему только плюсовиков интересует, что будет от i++ + ++i, а во всех остальных языках народ просто пытается отрабатывать свою зарплату и не плодить говнокод? Почему книжки Саттера и Александреску вроде «200 сложных задач на С++, которые сломают твой мозг» не только печатаются, но и покупаются, а в PHP, JS, Ruby и Java тонкости языка не выходят за пределы фриковских блогов?
Единственный язык, в котором так же любят изощряться как в плюсах — это, кажется, CSS. Но им-то простительно, они люди искусства, а тут — даже не знаю, как так…sand14
10.06.2018 09:32+2Почему только плюсовиков интересует, что будет от i++ + ++i
а в PHP, JS, Ruby и Java тонкости языка не выходят за пределы фриковских блогов?Почему же?
В официальных экзаменах по Java достаточно подобных вопросов.
Да и в прод коде проектов на C# доводилось встречать подобное — кстати, по большей части от разрабов, которые не знают как следует язык.
К счастью, более новые языки (Swift, Kotlin) стараются избавляться от такого.
(Что касается Ruby, то мой личный опыт его изучения и опрос коллег, которые с ним работали, показывает, что там как раз имеет значение знание всех тонкостей — и этого, там, пожалуй, слишком много. Но на поверхностный взгляд может показаться, что там все просто.)
clubs
10.06.2018 12:30Почему в примере «про move и лямбду» выводятся только 3 строчки в cout?
«return std::move(f);» в теле лямбды конструирует временный объект типа Foo («Foo(Foo&&)» или «Foo(const Foo&)» — не важно). Это 3-я строчка. Дальше нужно из этого временного объекта сконструировать объект f2. Значит, должна быть 4-я строчка?deviant_9
10.06.2018 16:37+1Стандарт явно разрешает (а в C++17 — даже требует) компилятору устранять временные объекты (а также, в меньшей степени, именованные) путём конструирования объектов непосредственно в конечной точке цепи копирований/перемещений.
en.cppreference.com/w/cpp/language/copy_elision
third112
10.06.2018 17:05Надеюсь, вы никогда не увидите это в реальном коде.
«Всё, что может пойти не так, пойдет не так» (Закон Мерфи).
Возможные причины:
1) Неопытный кодер может стремиться написать наиболее «крутой» код.
2) Более опытный кодер может:
2.1 «защитить» таким образом свой код;
2.2 просто схулиганить, пошутить над коллегами, сделать себя незаменимым (никто не сможет поддерживать его код) и т.д.
knstqq
10.06.2018 22:01+1> Или сделать конструктор от Foo(const Foo&&).
Вот кажется конструктор ПЕРЕМЕЩЕНИЯ от константы — максимально бессмысленная штука. Ведь переместить из Foo константного данные нельзя
heleo
Спасибо за головоломки в утро субботы)
Из выше перечисленного за всё время реально сталкивался только с проблемой похожей на пример inline, но только с учётом особенностей встраивания inline для debug\release.
А вот большинство из остальных примеров даже затрудняюсь как можно написать в здравом уме.
Enterindead
Последний пример в той или иной мере часто встречается на практике когда люди случайно пишут:
В намерениях было залочить мьютекс m_mtx, а в итоге объявляют локальную переменную типа unique_lock с именем m_mtx.