Не мешайте лодке плыть: об идеальном коде на C++ | OTUS

Не мешайте лодке плыть: об идеальном коде на C++

Cplus_Deep_14.3_site-4868-be09ae.png

Есть такая забава — плавать на лодках по прудикам и речкам. Мореманы хмыкнут и скажут, что не «плавают», а «ходят». Мы ИТ-шники. Нам можно и не такое.

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

Теперь две новости

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

Есть и хорошая новость, в отличие от бурной реки, на которой у нас просто нет времени, в отношениях с компилятором у нас есть возможность вдумчиво оттачивать нашу технику.

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

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

И вот опять, в который раз, я свидетель такой инициативы. Инициатива «сделать всё лучше» прекрасна, особенно в сочетании с хоть какими-то доказательствами её эффективности.

Итак, код сильно упрощён и очищен от смысла, оставлена лишь ключевая конструкция.

#include <iostream>

void foo(int num) {
  if (num == 0)
    std::cout << "zero" << std::endl;
  else if (num == 1)
    std::cout << "one" << std::endl;
  else if (num == 2)
    std::cout << "two" << std::endl;
  else if (num == 3)
    std::cout << "three" << std::endl;
}

Теперь, когда вы посмотрели этот код, я ещё раз повторю, что оставлена лишь ключевая конструкция.

Итак, где хочется помочь: изменить на switch, где же ещё. Получаем:

#include <iostream>

void foo(int num) {
  switch (num) {
  case 0:
    std::cout << "zero" << std::endl;
    break;
  case 1:
    std::cout << "one" << std::endl;
    break;
  case 2:
    std::cout << "two" << std::endl;
    break;
  case 3:
    std::cout << "three" << std::endl;
    break;
  }
}

Казалось бы совсем не похоже: переменную читаем один раз и выражаем своё намерение более явно. Прежде чем делать такой вывод, крайне полезно посмотреть получившийся код. Для этого нам может понадобится либо набор специальных ключей для компилятора, либо, что гораздо проще и нагляднее, использовать online-сервис.

Что мы увидим в первом и втором случаях?

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

Торопиться не будем, ведь если бы наш switch был хотя бы на один case длиннее, то код получился бы совершенно иной. Он был бы похож на таблицу указателей, которая индексируется нашим значением num. Обратите внимание, если бы case-ы были в более крупных диапазонах, например, от 1000 до 1020, то это бы не помешало компилятору построить таблицу переходов, просто перед индексацией будет вычтено значение 1000.

Даже если бы мы не для всех значений написали case, это не помешало бы содержать в таблице пустышки.

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

И напоследок, чтобы не сильно углубляться в дебри ассемблера, хочется показать к какому кода на самом деле ближе то, что сделал компилятор и в первом и во втором случае.

#include <iostream>

void foo(int num) {
  const char* p;
  if (num == 0)
    p = "zero";
  else if (num == 1)
    p = "one";
  else if (num == 2)
    p = "two";
  else if (num == 3)
    p = "three";

  std::cout << p << std::endl;
}

Фактически наша строка была вынесена в отдельную переменную, а её печать выполнялась в конце.

Желаю всем интересных экспериментов и открытий!

Не пропустите новые полезные статьи!

Спасибо за подписку!

Мы отправили вам письмо для подтверждения вашего email.
С уважением, OTUS!

Автор
1 комментарий
0

include <iostream>

void foo(int num) { const char* p[] = {"zero","one","two","three"}; std::cout << p[num] << std::endl; }

Для комментирования необходимо авторизоваться
Популярное
Сегодня тут пусто