Главная » Хабрахабр » [Перевод] «Современный» C++: сеанс плача с причитаниями

[Перевод] «Современный» C++: сеанс плача с причитаниями

Основные идеи: Здесь будет длиннющая стена текста, с типа случайными мыслями.

  1. В C++ очень важно время компиляции,
  2. Производительность сборки без оптимизаций тоже важна,
  3. Когнитивная нагрузка ещё важней. Вот по этому пункту особо распространяться не буду, но если язык программирования заставляет меня чувствовать себя тупым, вряд ли я его буду использовать или тем более — любить. C++ делает это со мной постоянно.

Блогпост «Standard Ranges» Эрика Ниблера, посвященный ренжам в C++20, недавно облетел всю твиттерную вселенную, сопровождаясь кучей не очень лестных комментариев (это ещё мягко сказано!) о состоянии современного C++.

Даже я внёс свою лепту (ссылка):

И да, я понимаю, что ренжи могут быть полезны, проекции могут быть полезны и так далее. Этот пример пифагоровых троек на ренжах C++20, по моему, выглядит чудовищно. Зачем кому-то может понадобиться такое? Тем не менее, пример жуткий.

Давайте подробно разберём всё это под катом.

Всё это немножко вышло из-под контроля (даже спустя неделю, в это дерево тредов продолжали прилетать комментарии!).

«Горстка озлобленных чуваков из геймдева» год назад наезжала на объяснение сути Boost. Теперь, надо извиниться перед Эриком за то, что я начал с его статьи; мой плач Ярославны будет, в основном, об «общем состоянии C++». Geometry примерно тем же способом, и то же происходило по поводу десятков остальных аспектов экосистемы C++.

Придётся развернуть мысль прямо здесь и сейчас! Но знаете, Твиттер — это не самое лучшее место для деликатных разговоров, и т.д и т.п.

Держите полный текст примера из поста Эрика:

// Пример программы на стандартном C++20.
// Она печатает первые N пифагоровых троек.
#include <iostream>
#include <optional>
#include <ranges> // Новый заголовочный файл! using namespace std; // maybe_view создаёт вьюху поверх 0 или 1 объекта
template<Semiregular T>
struct maybe_view : view_interface<maybe_view<T>> T const *begin() const noexcept { return data_ ? &*data_ : nullptr; } T const *end() const noexcept { return data_ ? &*data_ + 1 : nullptr; }
private: optional<T> data_{};
}; // "for_each" создает новую вьюху, применяя
// трансформацию к каждому элементу из изначального ренжа,
// и в конце полученный ренж ренжей делает плоским.
// (Тут используется синтаксис constrained lambdas из C++20.)
inline constexpr auto for_each = []<Range R, Iterator I = iterator_t<R>, IndirectUnaryInvocable<I> Fun>(R&& r, Fun fun) requires Range<indirect_result_t<Fun, I>> { return std::forward<R>(r) | view::transform(std::move(fun)) | view::join; }; // "yield_if" берёт bool и значение, // возвращая вьюху на 0 или 1 элемент.
inline constexpr auto yield_if = []<Semiregular T>(bool b, T x) { return b ? maybe_view{std::move(x)} : maybe_view<T>{}; }; int main() { // Определяем бесконечный ренж пифагоровых троек: using view::iota; auto triples = for_each(iota(1), [](int z) { return for_each(iota(1, z+1), [=](int x) { return for_each(iota(x, z+1), [=](int y) { return yield_if(x*x + y*y == z*z, make_tuple(x, y, z)); }); }); }); // Отображаем первые 10 троек for(auto triple : triples | view::take(10)) { cout << '(' << get<0>(triple) << ',' << get<1>(triple) << ',' << get<2>(triple) << ')' << '\n'; }
}

Пост Эрика появился из его же более раннего поста, написанного пару лет назад, который в свою очередь являлся ответом на статью Бартоша Милевского «Getting Lazy with C++», в котором простая сишная функция для распечатки первых N пифагоровых троек выглядела так:

void printNTriples(int n)
{ int i = 0; for (int z = 1; ; ++z) for (int x = 1; x <= z; ++x) for (int y = x; y <= z; ++y) if (x*x + y*y == z*z) { printf("%d, %d, %d\n", x, y, z); if (++i == n) return; }
}

Там же были перечислены проблемы с этим кодом:

Но что если, например, вместо распечатывания на экране вы захотите рисовать тройки как треугольники? Всё отлично, пока вы не хотите изменять или переиспользовать этот код. Или вдруг вы захотели остановиться сразу же, как одно из чисел достигло сотни?

Конечно, это действительно какой-то способ решить данные проблемы, ведь в языке C++ для этой задачи недостаточно встроенной функциональности, которая есть в каком-нибудь Haskell и других языках. После чего ленивые вычисления со сборкой списков (list comprehensions) представляются как главный способ решать этим проблемы. Но до этого мы ещё доберёмся. C++20 получит больше для этого встроенных ништяков, на что и намекает пост Эрика.

Держите законченную программу, которая распечатывает первую сотню троек: Так, давай вернёмся к стилю решения задачи, основанному на простом C/C++ («простом» — в смысле, «подходит, пока не нужно модифицировать или переиспользовать», по версии Бартоша).

// simplest.cpp
#include <time.h>
#include <stdio.h> int main()
{ clock_t t0 = clock(); int i = 0; for (int z = 1; ; ++z) for (int x = 1; x <= z; ++x) for (int y = x; y <= z; ++y) if (x*x + y*y == z*z) { printf("(%i,%i,%i)\n", x, y, z); if (++i == 100) goto done; } done: clock_t t1 = clock(); printf("%ims\n", (int)(t1-t0)*1000/CLOCKS_PER_SEC); return 0;
}

Сборка занимает 0. Вот как её можно собрать: clang simplest.cpp -o outsimplest. 9GHz; компилятор — Xcode 10 clang). 064 секунды, на выходе имеем экзешник размером 8480 байтов, который отрабатывает 2 миллисекунды и потом печатает числа (всё это на моём железе: 2018 MacBookPro; Core i9 2.

(3,4,5)
(6,8,10)
(5,12,13)
(9,12,15)
(8,15,17)
(12,16,20)
(7,24,25)
(15,20,25)
(10,24,26)
...
(65,156,169)
(119,120,169)
(26,168,170)

Это был дефолтная, неопитмизированная («Debug») сборка; давайте теперь соберём с оптимизациями («Release»): clang simplest.cpp -o outsimplest -O2. Стоять! 071 секнду на компиляцию и на выходе получится экзешник того же размера (8480 байт), который работает за 0 миллисекунд (то есть, ниже чувствительности таймера clock()). Это займёт 0.

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

Это может выглядеть так: Что бы сделал я — простейшую и из простых вещей, создать нечто пригодное для вызова, что будет возвращать следующую тройку.

// simple-reusable.cpp
#include <time.h>
#include <stdio.h> struct pytriples
{ pytriples() : x(1), y(1), z(1) {} void next() { do { if (y <= z) ++y; else { if (x <= z) ++x; else { x = 1; ++z; } y = x; } } while (x*x + y*y != z*z); } int x, y, z;
}; int main()
{ clock_t t0 = clock(); pytriples py; for (int c = 0; c < 100; ++c) { py.next(); printf("(%i,%i,%i)\n", py.x, py.y, py.z); } clock_t t1 = clock(); printf("%ims\n", (int)(t1-t0)*1000/CLOCKS_PER_SEC); return 0;
}

Отладочный экзешник вырастает на 168 байт, релизный остаётся того же размера. Оно собирается и работает примерно за то же самое время.

Поэтому я просто зову его сто раз, и каждый раз распечатываю результат на экран. Я сделал структуру pytriples, для которой каждый следующий вызов next() переходит к следующей валидной тройке; вызывающий код может делать с этим результатом всё, что душе угодно.

Совершенно ясно, как он делает то, что он делает (несколько ветвлений и простые операции над целыми числами), но далеко не сразу понятно что именно он делает на высоком уровне. Несмотря на то, что реализация является функционально эквивалентной тому, что делал цикл из трёх вложенных for-ов в изначальном примере, в реальности он стал гораздо менее очевидным, по крайней мере, для меня.

Если бы в C++ было чего-нибудь вроде концепции корутин, стало бы возможно реализовать генератор троек, такой же лаконичный, как вложенные циклы в изначальном примере, но при этом не имеющий ни одну из перечисленных «проблем» (Джейсон Мейзель именно об этом говорит в статье «Ranges, Code Quality, and the Future of C++»); это могло быть нечто вроде (это предварительный синтаксис, потому что в стандарте C++ корутин нет):

generator<std::tuple<int,int,int>> pytriples()
{ for (int z = 1; ; ++z) for (int x = 1; x <= z; ++x) for (int y = x; y <= z; ++y) if (x*x + y*y == z*z) co_yield std::make_tuple(x, y, z);
}

Давайте взглянем на пост Эрика, на основную часть кода: Может ли стиль записи в виде ренжей C++20 более ясно справиться с этой задачей?

auto triples = for_each(iota(1), [](int z) { return for_each(iota(1, z+1), [=](int x) { return for_each(iota(x, z+1), [=](int y) { return yield_if(x*x + y*y == z*z, make_tuple(x, y, z)); }); }); });

По мне так, подход с корутинами, описанный выше, куда как более читабельный. Каждый решает за себя. это греческая буква, глядите какой я умный!») — обе этих вещи выглядят громоздко и нескладно. Тот способ, которым в C++ создаются лямбды, и то, как в стандарте C++ придумали записывать вещи особо умным способом («что такое йота? Множество return-ов кажется необычным, если читатель привык к императивному стилю программирования, но возможно, к этому можно и привыкнуть.

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

Тем не менее, я отказываюсь верить, что мы, простые смертные без докторской степени в C++, сможем написать утилиты, необходимые для работы вот такого кода:

template<Semiregular T>
struct maybe_view : view_interface<maybe_view<T>> { maybe_view() = default; maybe_view(T t) : data_(std::move(t)) { } T const *begin() const noexcept { return data_ ? &*data_ : nullptr; } T const *end() const noexcept { return data_ ? &*data_ + 1 : nullptr; }
private: optional<T> data_{};
};
inline constexpr auto for_each = []<Range R, Iterator I = iterator_t<R>, IndirectUnaryInvocable<I> Fun>(R&& r, Fun fun) requires Range<indirect_result_t<Fun, I>> { return std::forward<R>(r) | view::transform(std::move(fun)) | view::join; };
inline constexpr auto yield_if = []<Semiregular T>(bool b, T x) { return b ? maybe_view{std::move(x)} : maybe_view<T>{}; };

Я программировал в основном на C++ все последние 20 лет. Быть может, что для кого-то это язык родной, но для меня всё это ощущается как если бы кто-то решил, что Perl излишне читабельный, а Brainfuck — излишне нечитабельный, поэтому давайте целиться между ними. Может быть, я слишком тупой, чтобы во всём этом разобраться, отлично.

И да, конечно, maybe_view, for_each, yield_if — все они являются «переиспользуемыми компонентами», которые можно перенести в библиотеку; эта тема, про которую я расскажу… да прямо сейчас.

Существует как минимум два понимания производительности:

  1. Во время компиляции
  2. Во время выполнения неоптимизированной сборки

Продолжим иллюстрировать это на примере примера с пифагоровыми тройками, но на самом деле, эти проблемы справедливы для множества других фичей C++, реализованных как часть библиотек, а не как часть синтаксиса.

Финальная версия C++20 ещё не вышла, поэтому для быстрой проверки я взял текущее лучшее приближение ренжей, коим является range-v3 (написанное самим Эриком Ниблером), и собрал относительно него канонический пример с пифагоровыми тройками.

// ranges.cpp
#include <time.h>
#include <stdio.h>
#include <range/v3/all.hpp> using namespace ranges; int main()
{ clock_t t0 = clock(); auto triples = view::for_each(view::ints(1), [](int z) { return view::for_each(view::ints(1, z + 1), [=](int x) { return view::for_each(view::ints(x, z + 1), [=](int y) { return yield_if(x * x + y * y == z * z, std::make_tuple(x, y, z)); }); }); }); RANGES_FOR(auto triple, triples | view::take(100)) { printf("(%i,%i,%i)\n", std::get<0>(triple), std::get<1>(triple), std::get<2>(triple)); } clock_t t1 = clock(); printf("%ims\n", (int)(t1-t0)*1000/CLOCKS_PER_SEC); return 0;
}

4. Я использовал версию после 0. -std=c++17 -lc++ -o outranges. 0 (9232b449e44 за 22 декабря 2018 года), и собрал с помощью команды clang ranges.cpp -I. 92 секунды, исполняемый файл получился размером 219 килобайт, а время выполнения увеличилось до 300 миллимекунд. Оно собралось за 2.

Оптимизированная сборка (clang ranges.cpp -I. И да, это сборка без оптимизаций. 02 секунды, экзешник выходит размером 13976 байтов, и выполняется за 1 миллисекунду. -std=c++17 -lc++ -o outranges -O2) компилируется за 3. Скорость выполнения в рантайме хороша, размер экзешника чуть увеличился, а вот время компиляции всё так же осталось проблемой.

Углубимся в подробности.

85 секунды дольше, чем версия с «простым C++». Время компиляции этого реально наипростейшего примера заняло на 2.

За три секунды современный CPU может произвести несметное число операций. Если вы вдруг подумали, что «меньше 3 секунд» — слишком маленькое время, то совершенно нет. За 0. Например, за какое время clang сможет скомпилировать настоящий полноценный движок базы данных (SQLite) в отладочном режиме, включая все 220 тысяч строчек кода? В какой такой вселенной стало нормальным, чтобы тривиальный пример на 5 строчек компилировался в три раза дольше целого движка баз данных? 9 секунд на моём ноутбуке.

Не верите мне? Время компиляции С++ было источником боли на всех нетривиальных по размеру кодовых базах, где я работал. Среди множества вещей, которые действительно хочется иметь в C++, вопрос времени компиляции, наверное, на самом первом месте списка, и был там всегда. Хорошо, попробуйте собрать какую-нибудь из широкоизвестных кодовых баз (Chromium, Clang/LLVM, UE4, и так далее отлично подойдут для примера). Тем не менее, складывается ощущение, что сообщество C++ в массе своей притворяется, что это совсем даже и не проблема, и в каждой следующей версии языка они перекладывают в заголовочные файлы ещё больше разных вещей, ещё больше вещей появляется в шаблонном коде, который обязан быть в заголовочных файлах.

Но в Си есть тенденция хранить в заголовках только объявления структур и прототипы функций, в C++ же обычно нужно свалить туда все шаблонные классы/функции. В большинстве своём это связано с доисторической концепцией «просто скопипастим всё содержимое файла» модели #include, унаследованной из Си.

8 мегабайтов, и всё это в заголовочных файлах! range-v3 представляет из себя кусок кода размером 1. В «простом C++» после всех преобразований получается 720 строк. Несмотря на то, что пример с сотней пифагоровых троек занимает 30 строчек, после обработки заголовков компилятору придётся скомпилировать 102 тысячи строк.

Справедливо. Но ведь именно для этого есть предкомпилированные заголовки и/или модули! — так и слышу, что вы это сейчас сказали. -std=c++17 -o pch.h.pch, скомпилируем с помощью pch: clang ranges.cpp -I. Давайте положим заголовки библиотеки ренжей в precompiled header (pch.h с текстом: #include <range/v3/all.hpp>, заинклудим получившийся pch.h, создадим PCH: clang -x c++-header pch.h -I. Время компиляции станет 2. -std=c++17 -lc++ -o outranges -include-pch pch.h.pch). То есть, PCH может сэкономить нам около 0. 24 секунды. С оставшимися 2. 7 секунды времени компиляции. 1 секундами они никак не помогут, и это куда дольше, чем подход с простым C++ 🙁

Возможно, замедление в 2 или 3 раза ещё можно считать приемлемым. В рантайме пример с ренжами оказался в 150 раз медленней. Больше, чем в сто раз медленней? Всё, что в 10 раз медленней можно отнести в категорию непригодного к использованию. Серьёзно?

Я работаю в индустрии видеогейминга; по чисто практическим причинам это означает, что отладочные сборки игрового движка или тулинга не смогут обрабатывать настоящие игровые уровни (производительность даже не приблизится к необходимому уровню интерактивности). На реальных кодовых базах, решающих реальные проблемы, разница в два порядка означает, что программа просто не сможет обработать настоящий объем данных. Неприятно, раздражающе тормозно. Возможно, существует такая индустрия, в которой можно запустить программу на наборе данных, подождать результата, и если это займет от 10 до 100 раз больше времени в отладочном режиме, это будет «досадно». Вы буквально не сможете играть в игру, если она рендерит изображение со скоростью всего 2 кадра в секунду. Но если делается нечто, обязанное быть интерактивным, «досадно» превращается в «неприменимо».

Бесплатные абстракции до тех пор, пока вам неинтересно время компиляции и возможно использовать оптимизирующий компилятор. Да, сборка с оптимизациями (-O2 в clang) работает со скоростью «простого C++»… ну да, ну да, «zero cost abstractions», где-то слышали.

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

Побочными эффектами стало ускорение компиляции (исходник) и упрощение отладки, поскольку реализация STL от Microsoft адски помешана на вложенных вызовах функций. Arseny Kapoulkine делал крутой стрим «Optimizing OBJ loader» на YouTube, там он упёрся в проблему тормознутости отладочной сборки, и сделал её в 10 раз быстрее, выбросив некоторые куски STL (коммит).

Это не к тому, что «STL — плохо»; возможно написать такую реализацию STL, которая не будет тормозить десятикратно в неоптимизированной сборке (EASTL и libc++ так умеют), но по какой-то причине Microsoft STL невероятно сильно тормозит потому, что они излишне сильно заложились на принцип «инлайнинг всё починит».

Всё что мне известно изначально — «STL тормозит в отладочном режиме», и я бы предпочёл, чтобы кто-то это исправил уже. Как пользователю языка, мне всё равно, чья это проблема! Ну или мне придётся искать альтернативы (например, не использовать STL, самостоятельно написать нужные лично мне вещи, или вообще отказаться от C++, как вам такое).

Давайте коротко взглянем на очень схожую реализацию «лениво вычисляемых пифагоровых троек» на C#:

using System;
using System.Diagnostics;
using System.Linq; class Program
{ public static void Main() { var timer = Stopwatch.StartNew(); var triples = from z in Enumerable.Range(1, int.MaxValue) from x in Enumerable.Range(1, z) from y in Enumerable.Range(x, z) where x*x+y*y==z*z select (x:x, y:y, z:z); foreach (var t in triples.Take(100)) { Console.WriteLine($"({t.x},{t.y},{t.z})"); } timer.Stop(); Console.WriteLine($"{timer.ElapsedMilliseconds}ms"); }
}

Сравните вот эту строчку на C#: По мне так, это кусок весьма и весьма читабелен.

var triples = from z in Enumerable.Range(1, int.MaxValue) from x in Enumerable.Range(1, z) from y in Enumerable.Range(x, z) where x*x+y*y==z*z select (x:x, y:y, z:z);

с примером на C++:

auto triples = view::for_each(view::ints(1), [](int z) { return view::for_each(view::ints(1, z + 1), [=](int x) { return view::for_each(view::ints(x, z + 1), [=](int y) { return yield_if(x * x + y * y == z * z, std::make_tuple(x, y, z)); }); });
});

А вам? Мне ясно видно, что здесь чище написано. Если честно, то альтернатива на C# LINQ тоже выглядит перегруженной:

var triples = Enumerable.Range(1, int.MaxValue) .SelectMany(z => Enumerable.Range(1, z), (z, x) => new {z, x}) .SelectMany(t => Enumerable.Range(t.x, t.z), (t, y) => new {t, y}) .Where(t => t.t.x * t.t.x + t.y * t.y == t.t.z * t.t.z) .Select(t => (x: t.t.x, y: t.y, z: t.t.z));

Я использую Mac, поэтому запустив на компиляторе Mono (который тоже написан на C#) версии 5. Сколько собирается этот код на C#? 20 секунд. 16 команду mcs Linq.cs получилось скомпилировать второй пример за 0. 17 секунд. Эквивалентный пример на «простом C#» уложился в 0.

03 секунды работы компилятора. То есть, ленивые вычисления в стиле LINQ добавляют 0. Сравните с дополнительными 3 секундами для C++ — это в 100 раз больше!

Да, в какой-то степени.

Похоже, всё же не увольняют, потому что в прошлом году я обнаружил, что кто-то добавил Boost. Например, мы здесь в Unity любим шутить, что «за добавление в проект Boost можно оказаться уволенным по статье». Asio, всё стало дико медленно собираться, и мне пришлось разбираться с тем, что простое добавление asio.h инклудит за собой весь <windows.h>, со всеми кошмарными макросами внутри.

У нас есть собственные контейнеры, созданные по той же причине, что описаны во введении к EASTL — более однообразный способ доступа, работающий между различными платформами/компиляторами, более хорошая производительность в сборках без оптимизаций, лучшая интеграция с нашими собственными аллокаторами памяти и трекингом аллокаций. По большей части мы стараемся не использовать и большую часть STL. Большая часть стандартной библиотеки нам и не нужна совсем. Есть и кое-какие другие контейнеры, чисто по причинам производительности (unordered_map в STL даже по идее не может быть быстрой, поскольку стандарт требует использования separate chaining; наша же хэш-таблица использует вместо этого открытую адресацию).

Тем не менее.

а может и не быть). Требуется время, чтобы убедить каждого нового сотрудника (особенно джуниоров, только что вышедших из университета) что нет, «современный» C++ не означает автоматически, что он лучше старого (он может быть лучше! а может и нет). Или например, что «код на Си» не обязательно значит, что его сложно понимать и он весь завален багами (может быть, так и есть!

Другой (джуниор) подсел рядом и спросил, почему я выгляжу так, как будто готов ‎(ノಥ益ಥ)ノ ┻━┻, я сказал «ну, потому что пытаюсь понять этот код, но для меня он слишком сложный». Всего пару недель назад я жаловался всем и каждому, как я пытаюсь понять один конкретный кусок (нашего собственного) кода, и не могу, потому что этот код «слишком сложный» для меня. И я такой: «нет, в точности до наоборот!». Его мгновенная реакция была вроде: «о, это какой-то старый код в стиле Си?». Он не работал ни над большими кодовыми базами, ни над C или C++, но нечто уже убедило его, что нечитаемым должен быть именно код на Си. (Код, о котором идёт речь, был чем-то вроде шаблонного метапрограммирования). Я виню университет; обычно студентам сразу же втирают, что «Си — это плохо», и потом никогда не объясняют — почему; это оставляет неизгладимый отпечаток на неокрепшей психике будущих программистов.

Но обучать всех коллег вокруг весьма утомительно, поскольку слишком многие находятся под влиянием идей вроде «современное — значит хорошее», или «стандартная библиотека должна быть лучше, чем что угодно, что мы сможем написать сами». Поэтому да, я определённо склонен игнорировать те части C++, которые мне не нравятся.

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

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

Я помню, как был на средней стадии где-то лет 16 назад. В интернетах ходит шутка о стадиях развития программиста на C/C++. Не задаваясь вопросом, зачем это вообще делать. Был очень поражён Boost, в том смысле что: «вау, такие шутки можно делать, это так круто!».

Поразительно? Точно так же, ну например, автомобили Formula 1 или гитары с тремя грифами. Чудо инженерной мысли? Конечно. Требует огромного скилла, чтобы управляться с ними? Безусловно. Не является правильным инструментом для 99% ситуаций, в которых вы когда либо находились? Да! Точняк.

Кристер Эриксон красиво сказал об этом здесь:

Не «писать код». Цель программиста в том, чтобы делать поставки в срок и в рамках бюджета. Решает пункт 2. Имхо, большинство сторонников современного C++ 1) придают чрезмерное значение исходному коду вместо 2) времени компиляции, отладки, когнитивной нагрузки, создаваемой новыми концепциями и добавленной сложностью, требованиями проекта, и так далее.

Некоторые так и делают. И да, люди, обеспокоенные состоянием C++ и стандартных библиотек, конечно, могут объединить усилия и попытаться улучшить их. Некоторые игнорируют куски стандартов и делают свои собственные параллельные библиотеки (вроде EASTL). Некоторые слишком заняты (или они так думают) чтобы тратить время на комитеты. Некоторые пришли к выводу, что C++ уже не спасти, и пытаются сделать собственные языки (Jai) или перепрыгнуть на другую лодку (Rust, подмножества C#).

Я работаю, возможно, над самым популярным в мире игровым движком, которым пользуются миллионы, и часть из них любит говорить, прямо или непрямо, насколько он отвратительный. Я знаю, насколько это неприятно, когда «куча озлобленных людей в интернете» пытается сказать, что вся твоя работа — лошадиного навоза не стоит. Печально! Это тяжело; я и другие коллеги вложили в это столько раздумий и усилий, и вдруг кто-то проходит мимо и говорит, что мы тут все идиоты и наша работа — мусор.

Они годами работали над чем-то важным, и тут куча Разъярённых Жителей Нижнего Интернета пришла и расфигачила твою любимую работу. Скорей всего, что-то подобное испытывает каждый, кто работает над C++, STL или любой другой широко используемой технологией.

Обычно — не самая конструктивная. Слишком легко перейти в защитную позу, это наиболее естественная реакция.

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

Нужно забыть о «себе» и «своей работе», и принять их точку зрения. Что я делаю, когда кто-то жалуется на вещь, над которой я работал? Задача любого софта/библиотек/языков — помочь пользователям решить их проблемы. С чем они пытаются разобраться, какие проблемы пытаются решить? Это может быть или идеальный инструмент для решения этих проблем, или «ок, это может сработать», или совершенно ужасно плохое решение.

  • «Я очень упорно над этим работал, но да, похоже что мой инструмент не очеь хорош в решении ваших проблем» — это совершенно правильный исход!
  • «Я очень упорно над этим работал, но не знаю или не учёл ваших потребностей, давайте я разберусь, что здесь можно сделать» — это тоже отличный исход!
  • «Простите, я не понимаю вашей проблемы» — тоже подойдёт!
  • «Я очень упорно над этим работал, но похоже, ни у кого нет проблем, которые решает моя работа» — очень печальных исход, но он может случиться, и случается на практике.

Точно так же, защита архитектуры библиотеки с помощью аргумента вида «это была популярная бибилиотека в Boost!» не учитывает той части мира C++, которая не считает, что Boost — это что-то хорошее. Некоторые из ответов вида «весь фидбек будет проигнорирован, если он не оформлен в виде документа, представленного на собрании комитета C++», которые я видел в последнее время не кажутся мне продуктивным подходом.

Игровые технологии традиционно создаются с помощью C или C++ просто потому, что вплоть до самого последнего времени остальные системные языки программирования просто не существовали (но теперь есть как минимум Rust, составляющий достойную конкуренцию). Индустрия видеогейминга, если смотреть глобально, тоже виновата. Учитывая ту зависимость от C++, в которую попала индустрия, она совершенно точно не проделала достаточной работы, чтобы её замечали, и не занимается достаточно улучшением языка, библиотек и экосистемы.

И кто бы ни начал работать над будущим C++, это самое будущее не в решении «непосредственных проблем» (вроде поставки игры или чего-то такого); они должны работать над чем-то куда более долговременным. Да, это тяжелая работа, и да — жаловаться в интернете куда проще. Если это будет стоить того, но знаете, это как-то лицемерно, говорить «C++ — фигня полная, нам это не нужно», и при этом никогда не доносить разработчикам языка, что же вам нужно. Существуют компании, которые могут это позволить; любая компания, производящая большой игровой движок или большой издатель с централизованной технологической группой совершенно точно может этим заняться.

Но есть тенденция игнорировать то, что добавилось в стандартные библиотеки, как по причине описанных выше проблем в архитектуре и реализациях STL (долгое время компиляции, плохая производительность в отладке), так и просто потому, что они эти дополнения недостаточно вкусные, или компании уже написали свои собственные контейнеры/строки/алгоритмы/… многие годы назад, и не понимают, зачем им менять то, что уже работает. Моё впечатление от всего этого в том, что большинство игровых технологий чувствуют себя достаточно хорошо с последними (C++11/14/17) нововведениями в сам язык C++ — например, полезными оказались лямбды, constexpr if очень крут, и так далее.

19-20 апреля в Москве состоится конференция C++ Russia 2019. Минутка рекламы. Один из наших гостей — Arno Schödl, отличный докладчик и CTO компании Think-Cell, расскажет про «Text Formatting For a Future Range-Based Standard Library». Будет множество хардкорных докладов, всё как вы любите. Вы его нашли. Искали место, где можно обсудить ренжи и другие новые фичи? Как попасть на конференцию, можно узнать на официальном сайте (с первого февраля цены на билеты повысятся).


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Из песочницы] ВИЧ – методы лечения от первых лекарств до сегодняшнего дня

Прежде, чем приступить к изложению материала, хотелось бы сказать несколько слов о себе: участник сообществ по борьбе с отрицанием ВИЧ („ВИЧ/СПИД диссидентством“): в 2016-2018 годах „ВИЧ/СПИД диссиденты и их дети“, с 2018 года – „ВИЧ/СПИД отрицание и альтернативная медицина“. Это ...

Изюминки прошедшей Moscow Python Conf++ 2019: трансформация в площадку для общения

Самыми горячими темами Moscow Python Conf++ оказались асинхронная разработка, а также сопоставление Python, его лучших практик и инструментария с аналогами из других языков, и его место в ландшафте современной разработки. Плюс мы пригласили выступить Бенджамина Петерсона, одного из разработчиков CPython, ...