Pull to refresh

Как не надо писать код

Reading time 4 min
Views 36K

image


Готовы погрузиться с головой в дивный мир программирования? Хотите узнать как непредсказуемо могут повести себя несколько простых строк кода?


Если ваш ответ "Да!" — добро пожаловать под кат.


Вас будут ждать несколько занимательных задачек на С или С++.


Правильный ответ с объяснением всегда будет спрятан под спойлером.


Удачи!


Про самую короткую программу


main;

Что будет если скомпилировать эту программу компилятором языка C?


  1. Не cкомпилируется.
  2. Не слинкуется.
  3. Скомпилируется и слинкуется.

Ответ:

Это валидный код на языке 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!"?


  1. 1000
  2. меньше
  3. больше

Ответ:

IO операции буферизуется для улучшения производительности.
Вызов fork() породит новый процесс, с copy-on-write дубликатом адресного пространства.
Буферизованные строчки будут напечатаны в каждом процессе.


Про индексы


#include <iostream>

int main()  {
    int array[] = { 1, 2, 3 };
    std::cout << (4, (1, 2)[array]) << std::endl;
}

Что напечатет этот код?


  1. 1
  2. 2
  3. 3
  4. 4
  5. Ошибка компиляции
  6. Не определено стандартом.

Ответ:

Порграмма напечатает 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. ~ 1 мс.
  2. ~ 100 мс.
  3. ~ 1 cек.
  4. ~ 1 мин.
  5. ~ 1 час.
  6. ~ 1 год.
  7. больше времени жизни вселенной.

Ответ:

Ха-ха! Вот и не угадали. Зависит от компилятора.
На моём ноутбуке 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;
}

Какую строчку программа напечатает последней?


  1. Foo()
  2. Foo(Foo&&)
  3. 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;
}

Что произойдёт если попытаться скомпилирвать и запустить это?


  1. напечатает 0
  2. напечатает 1
  3. напечатает 0x0
  4. не скомпилируется
  5. не слинкуется

Ответ:

Программа напечатает 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. Что произойдёт если ее запустить?


  1. Напечатается 120.
  2. Может произойти что угодно.

Ответ:

Может произойти что угодно. Это же С++.
Весь подвох в слове 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(); }

Какая строчка будет напечатана последенй?


  1. Foo(int, int)
  2. Foo(const Foo&, int)
  3. Foo(int, const Foo&)
  4. Foo(int)

Ответ:

Последней строчкой будет Foo(const Foo&, int).
Foo(i) — объявелние переменной, оно эквивалентно Foo i, а значит поле класса i пропадёт из области видимости.


Заключение


Надеюсь, вы никогда не увидите это в реальном коде.

Tags:
Hubs:
+43
Comments 94
Comments Comments 94

Articles