Сказка про собес наоборот
Вы помните свое первое собеседование? Я свое помню отлично, преподаватель дольше обычного гонял мою плис в симуляторе Keil-C, придирался к любой мелочи, докапывался до каждой запятой в коде прошивки. А потом начал гонять по алгоритмам трассировки печатных плат, которые мы должны были проходить только в следующем семестре. Я уже мысленно готовился на допсу, видно же что валит, как и предыдущих двух одногруппников. Но в конце сдачи лабы по проектированию мк преподаватель спросил хочу ли я делать «железное железо для железной дороги?» ©. Студенту второго курса ИТМО, которого кормили родители, и подрабатывавшего разгрузкой вагонов ночами, это было сродни офферу в гугль. С тех пор я много раз побывал с обеих сторон стола, и в качестве испытуемого, и как придирчивый лид (отнюдь этим не горжусь, но и не стыжусь), и как группа поддержки у коллег из соседних отделов. Всегда хотелось надеяться, что наши собеседования — это не таинственные квесты, где каждая задача — это каст сложного заклятия, а ошибки не выкидывают с данжа.
Но сначала сказка о том, как к нам попал Миша: однажды в славном городе S погромист-джедай по имени Михаил отправился на собеседование в компанию «Кодозавры». Он был уверен, что знает все, и даже изучил новейшие фреймворки «NoScript» и «Unreal С--». Как он думал, ничто не может его остановить. Когда Миша пришел на собеседование, его встретил HR-менеджер по имени Анна. Она с улыбкой спросила: «Расскажите, как вы бы пояснили своей бабушке, что такое мьютекс?». Ага, прям с порога наш доблестный HR накинул Мишке затравочный вопрос. Ну, а чё, это ж сказка, герои на белых самокатах и все такое…
Почему вы выбрали нашу компанию?
Так что пусть наша Аня шарит в примитивах синхронизации. Мишка ушами не хлопал, улыбнулся и начал рассказывать про асинхронные задачи, колбэки и атомики. Любая бабушка будет гордиться таким внуком, подумал он. Затем Михаила пргиласили в избу, Анечка упорхнула на кухню варить кофф, не в силах осознать полученную информацию. Ой, нет — она пошла дописывать тестовое задание для новичков. Впрочем неважно, это же сказка — нашего героя провели по офису, показали крепостных на полях румяных девопсов (блин, некрасиво получилось) и направили в переговорку, куда чуть позже подошел лид отдела, в который и хотел попасть тогда еще юный наш герой. Не давая никому в комнате расслабиться, он сразу выдал стандартную загадку: «Как вы перевернете строку задом наперед без использования дополнительной памяти?», и уселся напротив кандидата писать код в своем смак-буке, кинув на себя спелл невидимости, чтобы не мешать Мише думать. Заклинание невидимости не прошло проверку на интеллект, но богатырь не подал виду и попробовал решить задачу в уме, ведь листочка тоже не дали. В итоге в голове у него произошел целый карнавал символов и индексов, в конце концов, он пришел к решению и объявил его «Въ развращеніи словесъ, начнити отъ крещениа и двигатисѧ ко началу, мѣнити буквы, въ обратномъ порядкѣ». Сказанного хватило, чтобы в переговорку телепортнулся босс данжа, т.е. тех лид студии и начал кидать в Мишу файрболы, ой… задавать скользкие и каверзные вопросы. Мишаня отбивался как мог, не путался в ответах, в минуты передышек провел пару атак световыми мечами и давил противника вопросами о техстеке, билдферме и процессе ревью. Наконец мана у босса в кружке опустилась до критического уровня, на последнем глотке тех лид студии «Кодозавры» дал задание переписать пульт управления ракетой на NoScript. Для камлания оставили смак-бук, час времени и какую-то матерь, призыв духов Гугола и Яру особых успехов не принес, зато дважды являлась Аннафея и предлагала испить живой воды, вода правда была коричневого цвета, и отчего-то пахла кофем, который не Нескафе, но Мишаня в фей не верил и изгонял искусительницу вопросами про спинлоки, после чего Аннафея таяла легкой дымкой.
Звезды сошлись, код смешался с загадками, магия ctrl+shift+B позволила увидеть три заветных слова: «Скомпилилось без ошибок!». Из данжа Михаил выходил победителем, апнул уровень харизмы и поднял скилл уверенности в своих силах. Босс как обычно в досадной злобе буркнул свое финальное проклятие — «Мы перезвоним вам через неделю», и не перезвонил. И отлично, потому что, тогда бы Мишаня не получил оффер от Gaijin, где и начал свое приключение в мире 3d-движков, которые выжимают 60фпс на дохлом железе при сотнях активных объектов на сцене и пром плюсов в полный рост. Знакомо?
Мы вам перезвоним! Aга, после дождичка в полнолуние
Уфф… про световые мечи наверное было лишнее. Сказочным путем Мишани мне довелось пройти всего один раз, видимо сказывались знакомства. Через некоторое время я сам стал проводить собеседования, подбирая людей в команду. Тайны из наши собственных вопросов я никогда не делал, они както сами копились в текстовом файле и даже какое-то время висели на сайте компании в разделе «Карьера» и любой мог их посмотреть. Да и вопросами то их назвать сложно, скорее разные интересные фрагменты кода, которые ведут себя не так как предполагалось, какие-то сложнее, какие-то проще.
Поначалу, участвуя в других собеседованиях, я копировал и сами вопросы и устоявшиеся модели поведения. Но потом это стало надоедать, смысла в том что бы сильно налегать на матчасть или алгоритмы, если человек не впишется в команду не сильно много, если через пару месяцев придется искать нового. А ведь это и мои пара месяцев, которые я потрачу на обучение, отниму это время от своих задач и команды. Я не говорю, что не надо спрашивать алгоритмы, наоборот надо. Пусть человек сам расскажет какие знает и детально расскажет про пару по своему выбору. Перепробовав разные форматы собеседований, от сказочных, хорошо что был мудрый тех лид, который после первого такого праздника вызвал к себе и сказал, чтобы такой фигни он больше не видел, так что этот шаг мы проскочили сразу. Почти. Макс сорян, тебе просто не повезло, я был молод и глуп. Потом были стандартные на 10–15 вопросов на листе в стиле блиц опроса с минутой на обдумывание и небольшого тестового на час здесь и сейчас.
Позже мы с командой пришли к довольно необычному процессу отбора, назовем его «собес наоборот» — именно после таких собесов мы получили несколько отличных спецов в отдел, которые бы не попали к нам по другому. Как потом оказалось, один ненавидел тестовые и просто уходил с собеса, когда ему предлагали их делать, при этом у него шикарный гитхаб (https://github.com/megai2/d912 pxy) и человек в одиночку тянул нехилый пет-проект на 1к звездочек. Второй просто выгорел на прошлом месте работы, разочаровался в программировании и спотыкался на базовых вопросах по стандарту и стл, но раскрылся когда речь зашла о низкоуровневых оптимизациях на уровне асма, или надо было пореверсить алгоритм из асма и все подобное этому. Основной упор старались делать на тех людей, знаний которых не хватало в команде, даже если он не проходил по другим критериям. Не утверждаю, что это панацея, собес это в любом случае разговор двух сторон, которые пытаются продать себя друг другу. Как представитель компании, я продаю компанию человеку и смысла врать про пульт управления ракетой нет, если по выходу на пульте придется только перекрашивать кнопки и обновлять разметку. Хвала небесам, это не про игрострой, интересных задач тут навалом, успевай разгребать только.
А че, так можно было?
Со стороны человека который идет в компанию, (как мне кажется) тоже особо нет смысла преувеличивать свое умение чинить пульты разных видов, с плюсиками или бемолями, квадратные или змеевидные. Или хвастаться починкой пультов от Когдазавра и Роботоноида версии 2.0. Все равно квадратный зеленый пульт мы чинить не умеем, или умеем, но еще не знаем как и вечером придется читать документацию. Как представитель себя любимого, я стараюсь получить побольше денег и (поменьше звиздеца в любых проявлениях) интересные для меня качество и количество задач.
В чем идея «собеса наоборот», все тот же список вопросов, будь он неладен, только мы просили выбрать их по желанию и задать нам, сами отвечали на вопросы, иногда сознательно опуская некоторые моменты, иногда внося неточности, а наш будущий коллега, подмечал или не подмечал это. Добавлял и поправлял, или не поправлял наши рассказы, в большинстве случаев человека вовлекали в разговор.
На тех собесах я не помню чтобы мы писали сложный код, чаще вообще не писали, разве что были небольшие пояснения на бумаге. Ломалась сама атмосфера собеседования как экзамена, и вот уже кандидат пытается подловить нас на ответах, задает вопросы не из списка, несколько раз не могли потом успокоить по полчаса — вопросы сыпались один за другим, приходилось кастовать смак-бук и тестовое. Обращали внимание на умение человека ясно и внятно изложить свои мысли, проговорить идею, задать вопрос, потому что матчасть можно подтянуть, не сейчас так через месяц-другой. Скилл в починке скругленных серебристых тачскринов тоже со временем наработается, если человек не особо ленив. А вот культуру умения вести разговор, обмена идеями и обсуждения технических вопросов в ревью, не обидив при этом коллег и не вернув его 120 раз на доработку, осилить (а главное принять) намного сложнее.
Достаем TXT файлик, которому очень много лет
Собственно это все, что я хотел рассказать. А теперь танцы, т.е. вопросы. Часть вопросов уже поднимались вот тут (https://habr.com/ru/articles/764514/ дичь от Феди, я смотрю многим пришлась по вкусу), надеюсь эти будут не хуже, могут быть дубли. Какие-то вопросы покажутся вам простыми, значит вы много пишите кода, и вообще человек хороший:
0×1…
Начну пожалуй с классического вопроса, что будет выведено в результате работы программы?
void sayHello() {
std::cout << "Hello, World!\n";
}
void sayНello() {
std::cout << "Goodbye, World!\n";
}
int main() {
sayНello();
return 0;
}
А что не так?
А надо скомпилить, тогда в консоль будет выведено «Goodbye, World!». Потому что вторая sayHello написана с использование юникода, который к сожалению не всегда палится. И она же вызывается (clang, latest)
sayHello(): # @sayHello()
push rbp
mov rbp, rsp
mov rdi, qword ptr [rip + std::cout@GOTPCREL]
lea rsi, [rip + .L.str]
call std::basic_ostream >& std::operator<< >(std::basic_ostream >&, char const*)@PLT
pop rbp
ret
"_Z9sayНellov": # @"_Z9say\D0\9Dellov"
push rbp
mov rbp, rsp
mov rdi, qword ptr [rip + std::cout@GOTPCREL]
lea rsi, [rip + .L.str.1]
call std::basic_ostream >& std::operator<< >(std::basic_ostream >&, char const*)@PLT
pop rbp
ret
0×2…
Аэта функция обладает удивительной способностью изменять реальность в зависимости от того, сколько ног у ее вызывающего.
int abs_legs(int my_legs) {
if (my_legs < 0) {
return -my_legs;
}
}
А что не так?
Функция, возвращающая значение, должна возвращать значение (хм!) из всех возможных веток, потому что всегда найдется такое условие которое приведет к неопределенному поведению. Кажется это правило работает не только для функций, которые возвращают значение.
int abs_legs(int my_legs) {
if (my_legs < 0) {
return -my_legs;
}
return my_legs;
}
0×3…
Эта функция не раскрывается полностью, оставляя свою магию за покровом компилятора. Где деньги, Зин?
int get_money(int index, const int *pockets) {
int a = index + pockets[++index];
// ...
return a;
}
А что не так?
Компилятор может переставить порядок операции для index + pockets[++index], и это приведет к неоднозначному поведению с разными настройками оптимизации. Неупорядоченная или неопределенная последовательность операций приведет к сайд эффекту при работе с переменной index
int get_money(int index, const int *pockets) {
++index;
int a = index + pockets[index];
// ...
}
0×4…
Что будет выведено в bufMatrix?
void umcp_read_buffer_from_pipe() {
char bufKernel[12];
char bufMatrix[12];
std::cin.width(12);
std::cin >> bufKernel;
std::cin >> bufMatrix;
}
А что не так?
В этом примере первое чтение не приведет к переполнению, и заполнит bufKernel усеченной строкой. Но второе чтение может переполнить bufMatrix, чтобы этого не произошло надо также вызвать std: cin.width (12); перед получением bufMatrix. Или воспользоваться безопасной работой через строки.
void umcp_read_buffer_from_pipe() {
std::string bufKernel, bufMatrix;
std::cin >> bufKernel >> bufMatrix;
}
0×5…
Кажется, в этом коде нильзя-программер просто смешивает случайности с абсурдом и называет это «настройкой отображения».
std::string str_func();
void display_string(const char *);
void set_display_options() {
const char *str = str_func().c_str();
display_string(str);
}
А что не так?
Здесь std: string: c_str () вызывается для временного объекта std: string. Полученный указатель будет указывать на освобожденную, но возможно еще валидную, память после уничтожения объекта std: string в конце выражения присваивания. Правда не очень на это надейтесь, это приведет к неопределенному поведению при доступе этому указателя.
std::string str_func();
void display_string(const char *s);
void set_display_options() {
std::string str = str_func();
const char *cstr = str.c_str();
display_string(cstr);
}
0×6…
Сколько раз за вечер бар будет закрыт?
void ьщкт() {
int *bar = new int;
std::shared_ptr p1(bar);
std::shared_ptr p2(bar);
}
А что не так?
Здесь два несвязанных смарт указателя созданы на основе одного и того же базового значения. Когда время жизни переменная p2 закончится, она удалит ресурс, которым управляет. Об этом не узнает переменная p1, которая попробует выполнить удаление этого ресурса повторно, что-то да удалит в итоге. Но это уже совсем другая история.
void night() {
std::shared_ptr p1 = std::make_shared();
std::shared_ptr p2(p1);
}
0×7…
Почему утром опасно ходить в bar?
void morning(const std::string &owner) {
std::fstream bar(owner);
if (!bar.is_open()) {
// Handle error
return;
}
bar << "customer";
std::string str;
bar >> str;
}
А что не так?
Потому что можно не выйти. Данные добавляются в конец файла, а затем считываются из того же файла. Однако, поскольку поскольку указатель в файле остался на прежнем месте в конце, то попытка прочитать данные из конца файла приведет к UB. В бар надо ходить вечером, то все «замечательно выходит» ©.
void evening(const std::string &owner) {
std::fstream bar(owner);
if (!bar.is_open()) {
// Handle error
return;
}
bar << "customer";
std::string str;
bar.seekg(0, std::ios::beg);
bar >> str;
}
0×8…
Какое одно слово надо изменить, чтобы этот код заработал?
struct Foo {
void *foo;
struct foo *next;
};
static Foo foos;
static std::mutex m;
static std::condition_variable condition;
void consume_list_element(std::condition_variable &condition) {
std::unique_lock lk(m);
if (foos.next == nullptr) {
condition.wait(lk);
}
// Proceed when condition holds.
}
А что не так?
После того как переменная condition будет отпущена, надо проверить не забрал ли кто элемент пока мы переключали контекст.
while (foos.next == nullptr) {
condition.wait(lk);
}
0×9…
Почему этот rand () работает как генератор случайных чисел от «Sony»? Здесь надо немного пояснить про этот мем, для генерации rand () в своих секретных ключах Сони, использовала секретную функцию которая из-за ошибки реализации возвращала одного и тоже число.
int main() {
std::string id("ID");
id += std::to_string(std::rand() % 10000);
// ...
}
А что не так?
Полученное путем вызова функции rand () число предсказуемо и имеет ограниченную случайность. Чтобы избавиться от повторения сгенерированной последовательности в некоторых реализациях std: rand (), надо выбрать генератор, предоставляющий истинно неповторяющуюся последовательность. Например mtXXXX (https://dl.acm.org/doi/10.1145/272991.272995)
int main() {
std::string id("ID");
std::uniform_int_distribution distribution(0, 10000);
std::random_device rd;
std::mt19937 engine(rd());
id += std::to_string(distribution(engine));
// ...
}
0хА…
Получит ли ведьмак монеты?
class Witcher {
int i;
public:
virtual int TakeCoins(int l) { return i += 100; }
};
int main() {
Witcher *geralt = new Witcher();
memset(geralt, 0, sizeof(Witcher));
std::cout << geralt->TakeCoins(100);
return 0;
}
А что не так?
Лучше не использовать memset () для работы с полиморфик типами, она потрет указатель на vtbl в классе, такой объект становится невалидным и попытка поработать с ним будут приводить к UB
class Witcher {
int i = 0;
public:
virtual int TakeCoins(int l) { return i += 100; }
};
int main() {
Witcher *geralt = new Witcher();
std::cout << geralt->TakeCoins(100);
return 0;
}
0xB…
Станет ли бард пьяницей?
class Bard {
int _beer;
int _meal;
public:
Bard(int meal) : _meal(meal), _beer(_meal - 1) {}
};
А что не так?
Cписок инициализации мемберов нарушен и сначала будет установлена переменная _beer, а затем будет инициализирована переменная _meal. Поскольку порядок объявления переменны не соответствует тому что заявлено в описании класса, попытка прочитать значение _meal приводит к тому, что оно имеем неопределенное значение. Скорее всего бард сопьется без закуси.
class Bard {
int _beer;
int _meal;
public:
Bard(int beer) : _beer(beer), _meal(_beer + 1) {}
};
0хС…
В каком случае выстрел по ногам будет особенно результативным?
for (auto format = begin(formats), __end = end(formats); format != __end; ++format) {
if (snd::CodecNamesEq(....)) {
format.is_stereo = true;
formats.push_back(stereo_format);
}
}
А что не так?
Если в процессе push_back () произойдет реалокация formats, итератор format будет указывать уже на удаленную память, работа с которой вызовет кучу проблем.
0хD…
Этот код лучше попробовать собрать самому, чтобы понять админ ли вы? (https://onlinegdb.com/rMnPPq5et)
int main() {
std::string access_level = "user";
if (access_level.compare("user // Check if admin ")) {
std::cout << "You are an admin.\n";
}
return 0;
}
0xE…
Иногда в с++ просто вхух и работает!
.∧_∧
( ・ω・。)つ━☆・*。
⊂ ノ ・゜+.
しーJ °。+ *´¨)
.· ´¸.·*´¨) ¸.·*¨)
(¸.·´ (¸.·'* ☆ Wizz, just works! ☆
Но чаще приходится долго отлаживать
0xF…
Ни один Мишаня в результате наших собесов не пострадал, Костя ответил на все вопросы и добавил в список пару новых. Особо отличившиеся получили предложение присоединиться к команде, просто умные люди шли дальше на собес в iOS/Android team. А Максим потом нашел себя в бекэнд команде красного банка.
Вот и сказочке конец, а кто дочитал, благодарю за внимание.