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

http://aras-p.info/blog/2018/12/28/Modern-C-Lamentations/
  • Перевод

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


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

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



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


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

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



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


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


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


Пифагоровы тройки в стиле ренжей C++20


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


// Пример программы на стандартном 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>> {
  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_{};
};

// "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;
                }
}

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


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

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


Пифагоровы тройки в стиле простого C++


Так, давай вернёмся к стилю решения задачи, основанному на простом 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;
}

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


(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. Это займёт 0.071 секнду на компиляцию и на выходе получится экзешник того же размера (8480 байт), который работает за 0 миллисекунд (то есть, ниже чувствительности таймера clock()).


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


Может ли стиль записи в виде ренжей 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>{};
  };

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


И да, конечно, 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;
}

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


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


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


Время компиляции — огромная проблема для C++


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


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


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


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


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


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


Производительность сборки без оптимизаций — важна


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


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


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


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


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


Это не к тому, что «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));

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


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


Но ведь нельзя просто проигнорировать то, что не нравится?


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


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


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


Тем не менее.


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


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


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


Почему такое происходит с C++?


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


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


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


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


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


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

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


Принимаем и даём обратную связь


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


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


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


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


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


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

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


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


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


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


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

Комментарии 230

    +7
    Отличная статья!

    Спасибо. Читал в оригинале, поэтому пролистал — вроде все ок ))))
      +12
      Забавно, что автор перед публикацией тоже попросил прочесть, поправил пару ошибок и сказал «вроде все ок» :) Оказывается, он хоть и не пишет по-русски, но тексты понимает!
      +6
      Вот это вот все сверху написатое «на C++20» — это на самом деле отличная лакмусовая бумажка. Если вдруг оказывается, что от вас на собеседовании хотят чего-то в этом духе — это отличный повод задуматься «а надо ли оно мне?» и молча под шумок слинять.
        +8

        Ну, у меня в резюме написано, что я шарю в template/constexpr metaprogramming и в «C++17 and upcoming 20 standard». Как ещё это проверить?


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

        +3
        В интернетах ходит шутка о стадиях развития программиста на C/C++. Я помню, как был на средней стадии где-то лет 16 назад.


        У нас контора сейчас находится где-то примерно между 4 и 5 этапом.
        Большинство программистов — даже миновав стадию 3.
        А часть вообще смотрит в сторону js.

        Реально с++ развивается куда-то не туда.
        Появляются полезные штуки, но ещё больше полезных — не появляется. Даже в перспективе.
        Зато вместо этого ширится и глубится «шаблонная магия».
          +13
          Или как вариант, вы становетесь старпёрами :-) Как отследить этот момент и различить — когда технология действительно свернула куда-то не туда, и когда просто ты постарел и стал скучным?
            +2
            Становимся конечно.

            Только это не отменяет того, что в языке не появляются нужные для работы вещи.
            Т.е. речь идёт не о субъективной «скучности» языка, а об объективной «нужности».
            C++ становится интереснее, выразительнее наверное.
            Но… не становится более нужным.
              +1

              Ну вот ренжи решают вполне конкретную проблему, на этом примере — переиспользуемость, избегание дублирования, разделение сущности и алгоритма обработки. У вас нет такой проблемы? Есть ещё некая проблема записи в "функциональном стиле".


              А какие фичи бы вам хотелось иметь, что вам нужно согласно критерию "объективной нужности"?

                +4
                Хочется двоичной совместимости — как в C.
                Хочется нормальной интроспекции.
                Т.е. хочется больше фич в runtime, а у c++ все новые навороты — в compile-time.
                  +6
                  Хочется двоичной совместимости — как в C.

                  Стандарт С что-то говорит про двоичную совместимость?


                  Хочется нормальной интроспекции.

                  Так интроспекция будет выражаться через компилтайм-интроспекцию, для котоой шаблонной магии пока недостаточно (constexpr for, например). И это как раз вполне укладывается в рамки неплаты в рантайме за то, что не используешь.

                    +1

                    Не знаю что стандарт С говорит о двоичной совместимости, но на ней уже лет дцать построена и работает тьма проектов.
                    А с c++ ты даже за собственный проект не можешь ручаться: будет ли подключаться какая-нибудь СВОЯ библиотека при смене версии компилятора.


                    Про compile-time интроспекцию не совсем понятно. Интроспекция же это получение информации о внутренней структуре объекта в runtime. Этого не хватает.

                      +2
                      А с c++ ты даже за собственный проект не можешь ручаться: будет ли подключаться какая-нибудь СВОЯ библиотека при смене версии компилятора.

                      Последний раз, когда ломалась двоичная совместимость — при переходе в gcc 4 на gcc 5, когда по новому стандарту потребовалось изменить устройство некоторых вещей в STL. Да и то, по факту это можно было обойти, определив соответствующий дефайн.


                      Про compile-time интроспекцию не совсем понятно. Интроспекция же это получение информации о внутренней структуре объекта в runtime. Этого не хватает.

                      Рантайм вынесен на библиотечный уровень. Условно, предполагается, что будут какие-нибудь библиотеки, где будут регистрироваться нужные для рантайм-интроспекции классы. А компилтайм-интроспекция позволит соответствующие рантайм-описания построить.

                        +1

                        Ладно, давайте так:
                        Могу я взять либу, скомпилированную неизвестно кем неизвестно чем (но из c++ исходника), создать объект класса, который она экспортирует и пошариться по его внутренностям, повызывать методы, пообращаться к переменным?

                          +1

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

                            +1

                            Замечу, оверхед — по памяти.
                            В плане производительности, если тебе это не надо, то ты "за это не платишь".
                            Более того, это можно было бы вообще отключать опциями компиляции.
                            Тогда выдавалась бы ошибка, что интроспекция недоступна.


                            А так, c++ остаётся эдаким средством строительства высокоэффективных "кирпичей" (монолитов). Вся гибкость языка — в compile-time, а если нужно универсальное взаимодействие на двоичном уровне, то… C-style интерфейс. И это частично, неуклюже так, прикроет только один вопрос.

                              +1
                              Замечу, оверхед — по памяти.

                              А значит, и по производительности, если раньше у меня был мелкий объект на 16 байт, которых четыре в кешлайн влезало, то теперь там куча какой-то ерунды.


                              Более того, это можно было бы вообще отключать опциями компиляции.

                              Для одной структуры данных? Для всего TU?


                              Тогда выдавалась бы ошибка, что интроспекция недоступна.

                              Где, в рантайме? А в чём разница по сравнению с предлагаемым решением, только opt-in вместо opt-out?

                                +1

                                Информация о внутреннем устройстве это всё-таки принадлежность класса, а не объекта. Так что объект как был маленьким, так и останется.
                                И — да: отличие в том, что интроспекция по умолчанию включена, а не отключена, а самое главное — стандартна, а не то что один программист использует одну библиотеку, другой — другую.

                                  0
                                  Так что объект как был маленьким, так и останется.

                                  И даже указателя на описание класса нет?


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

                                    –1

                                    Да, какая-то отсылка на класс в объекте должна быть, Вы правы.
                                    Но это всё-таки указатель, а не массивная структура данных.

                                      –2
                                      Вспомните, что таблица виртуальных функций тоже по умолчанию не генерируется.

                                      В случае невиртуального класса можно добавить ещё один невиртуальный метод, который будет давать доступ к "классу объекта" с информацией о полях и методах. Конечно, если указатель на невиртуальный класс скастовать к void*, то не получится восстановить информацию, но это следствие гибкости С++ и с этим ничего не поделать. Информацию о полях и методах класса можно будет положить куда-нибудь в секцию констант рядом с описаниями других классов — и если к ним никто не будет обращаться, то, возможно, они даже в память не будут загружены. Единственный минус — бинарники будут больше.

                                        0
                                        Очевидно, такой метод будет давать описание только статического типа объекта, чего, вероятно, для практических применений недостаточно.
                            +2
                            Можно до посинения цитировать стандарты, но C предлагает вменяемый ABI, а C++ нет. Как-то так…
                              +3
                              Коллеги тут недавно рассказывали. Есть родная система А. Понадобилось интегрировать её с чужой системой Б. Для интеграции система Б отдает исходники SDK который собирается на стороне системы A и через него общается. Трахались какое-то время но в конце-концов подружили А и Б и все заработало ОК. Да, обе естественно на C++ (мы же не хипстеры какие это кровавый прод).

                              Через некоторое время систему А решили перевести на модерн С++. Ну в самом деле, GCC 4.x, C++98 и все такое уже почти 10ть лет. Доколе? ОК, взяли последний GCC (что мелочиться? чтобы сразу C++17) немного потрахались но перевели. Заработало. И стало вдруг всем тепло уютно и сухо.

                              Но вот незадача. Система Б без которой уже никуда под модерн С++ жить отказалась наторез. Даже собираться. Отказаться от ней нельзя. Поменять её нельзя. Как-либо повлиять на неё нельзя. В общем что дали с тем и живите.

                              ОК. Решили — а давайте поженим нашу А уже под модерт С++ с ихней Б собранной тем, древним, от мамонта. Собрали. И даже успешно слинковали что всех удивило. Но вот незадача: начало все валиться к чертям собачьим. Патамучта что-то там не так с разницей реализации в std::string и прочими мелкими ништяками STL. А они задействованы в API между А и Б.

                              В общем, пока не обрезали API почти до уровня plain C — ничего не заработало. А ведь казалось бы ну что нам стоит? Есть два проекта оба на С++ оба вроде почти вменяемые даже оба в исходниках — взять и поженить! А. Шас…
                                +3

                                Расскажите им там при случае про _GLIBCXX_USE_CXX11_ABI.

                                  0
                                  Перефразируя сказку о рыбаке и рыбке: "… много ли в _GLIBCXX_USE_CXX11_ABI корысти?!...", если для его соблюдения всеми нужными зависимостями и самой системой надо наизнанку вывернуться через накрест лежащее подпространство? По моему опыту, конечно.
                      +5
                      А какие фичи бы вам хотелось иметь, что вам нужно согласно критерию "объективной нужности"?

                      Хочу адекватные модули, но они, боюсь, принципиально нереализуемы с имеющимся набором констрейнтов.

                        +2

                        Вброшу свою ложечку. Судя по https://old.reddit.com/r/cpp/comments/akihlv/c_modules_might_be_deadonarrival/, эти самые констрейнты заключаются в "шоб мы могли свой код времён Кеннеди перенести в новый стандарт в 2 строчки".
                        Т.е. module lookup хотят сделать зависимым от содержимого файлов (а там имя модуля может задаваться в зависимости от макроссов, упс...) просто потому, что, грубо говоря, есть ОС, которая не умеет больше одного слоя директорий в своей ФС. Или ОС, которая не знает, что такое расширение. Или в байте 9 3/4 бит...

                          0
                          Да, и поэтому тоже.

                          Хотя упомянутую вами статью я не видел, но, по беглому просмотру, вроде даже согласен.
                        0
                        Хочется удобную систему модулей, а не то, что сейчас.

                        P.S. Блин, не долистал до конца ветки, уже сказали.
                      0
                      На самом деле все просто. «Не туда» это когда для того чтобы впсисать концепцию в язык приходиться измудряться со скобками. Даже в этой статье пример c# и linq. Хорошо читаемый и понятный код на шарпах… Представляю как будет выглядеть linq на с++.
                    +3
                    «Золотые, бессмертные строки Виталия Светославовича Луговского как нельзя лучше подходят к моменту»
                      0
                      … брайтонским батоном колбасы
                        0
                        … Кембриджским батоном лямбдоты.
                        Я функтором аппликативным
                        Eval, eval, eval, eval
                      +10
                      Почему-то люди считают, что если в язык добавили новые фичи, то ими нужно обязательно пользоваться. Хотя вам просто дают дополнительные возможности, которые следует применять там, где это оправдано.

                      И если что-то проще и удобнее выглядит в процедурном стиле или коде из 90-х, то зачем натягивать сову на глобус усложнять?
                        +10
                        Проблема в том, что этими фичами могут пользоваться другие разработчики, код которых приходится читать.
                          –4
                          Это если нет нормального ревью
                            +15
                            Этими фичами также могут пользоваться разработчики библиотек, которыми приходится пользоваться. И никакое ревью тут не поможет.

                            Это меня в Scala в свое время очень выбешивало. Я пытался использовать ее просто как java-с-плюшками, но постоянно натыкался на то, что популярные в Scala-мире библиотеки делают даже простейшие вещи какими-то очень сложными и неочевидными фичами языка, и мне волей-неволей приходится это все разбирать и изучать, чтобы их использовать.

                            С С++ я распрощался уже лет пять назад, и рад этому. Уже тогда это был перегруженный фичами язык с переусложненным синтаксисом, во что он там сейчас превратился мне страшно подумать.
                          0
                          Хотя вам просто дают дополнительные возможности, которые следует применять там, где это оправдано.
                          Осталось только узнать, какие фичи нужно использовать, и когда они оправданы. Тому кто варится в плюсах нонстопом последние лет 10 это может и не сложно. А я вот на них около 6 лет почти не писал, и когда недавно решил чуть освежиться и открыл cppreference.com, у меня глаза на лоб полезли от объема информации. Просто чтобы перечислить все новые термины, фичи, инструкции и функции нужно с десяток A4 газетным шрифтом. Каким образом новичку вообще определить, какую из них стоит изучить, а какую просто пропустить, не входя в рекурсию? Нанимать сеньора и проходить с ним по всему списку, чтобы он отметил все, что реально используют на практике и могут спросить на собеседовании?
                          0
                          Ну что ж, ещё один гвоздь в крышку гроба C++. Хорошо, что теперь появилась достойная альтернатива — Rust. В этой статье открыли (не в первый раз) проблемы C++, которые не решаются годами. И чем больше таких статей, тем быстрее люди поймут, что наиболее эффективным и безболезненным улучшением C++ для них будет переход на Rust.
                            +5
                            Ну вообщет эта проблема решается прямо сейчас. Вы даже можете всех участников легко и просто найти в интернетах, в Твиттере. Важно только не проходить мимо. Если вы сами не поучаствуете в обсуждении, то в конце концов всё решат за вас другие люди — и возможно тем способом, который вам не нужен.
                              +2
                              К сожалению решают «другие люди». Мы можем только «рублём/долларом проголосовать» за компилятор и своим открытым кодом без этого нового уродства (личное мнение).
                              Очень рад увидеть статью, в которой вижу единомышленников. Спасибо.
                                +3

                                А чо сразу рублём-то. Может, вначале пообщаться с авторами Стандарта? Или свою фичу подать на рассмотрение? У нас в России, кстати, есть три человека из Комитета. И один из них будет выступать на ближайшей C++ Russia.

                                  +4

                                  Можно предложить идею улучшения языка на stdcpp.ru еще, кстати

                                    0
                                    Ну как сказать. Мне вот не понравилась вполне конкретная мелочь в языке, то что в chrono теперь есть типы и month и months и они значат совершенно разное)
                                    я написал, предложил именование которое меня устроит, antoshka перевел и написал автору либы. Автор либы ответил что он лучше знает, и если у нас в голове что-то путается, это наши проблемы =) Дальше желание «улучшать стандарт С++» у меня пока отпало… если только сильно не припрет)
                                      0
                                      А можно более подробно? Потому что я уже боюсь, что я сам заранее запутался ))))
                                          0
                                          Почитал, мозг взорвался. Могу сказать одно — на таком языке с такими пропозалами (я не про Ваше уточнение — оно как раз по делу) я не хочу писать…
                                        0
                                        month — это момент времени, конкретный месяц; months — это интервал времени, разница между двумя моментами. В естественном языке то же самое. В чём тут путаница, и каким было ваше предложение?
                                          0
                                          выше ссылку кинул
                                            +2
                                            А, т.е. вопрос не к разнице типов, а к неоднозначности определения интервалов «месяц» и «год». Я согласен, что проблема есть, но я лично бы вообще не вводил в chrono неоднозначные интервалы вроде months/years и остановился бы максимум на weeks. Пусть пользователь (или дополнительная библиотека) сам решает, какой именно год (календарный, тропический, сидерический) или месяц (календарный, средний, лунный) ему нужен в каждом конкретном случае.
                                          +5

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

                                  +5
                                  какой же это гвоздь? Это просто реакция на слишком заумные предложения к стандарту. Мне лично некоторые предложения ranges понравились (сами интервалы — ranges), хотя эти представления (view) и может-быть-представления (maybe_view) я тоже считаю непонятыми и переусложненными.
                                    +15

                                    Думаю, что Rust недостаточно плох, чтобы стать действительно популярным и составить конкуренцию C++.

                                    +9
                                    Когда -то я думал «вот дураки, пишут на чистом Си до сих пор — сами себе отрубают возможности». А на самом деле на С++ простую задачу часто стараются решать именно как написано самом верхнем примере, когда в этом нет необходимости, когда в случае чего можно поменять две строчки простого и понятного когда. Так что получается пишущие на чистом Си как раз умные, отрубая возможности которые редко применяются, мешают распространению быдлокода.
                                      +7
                                      Hold down my void**
                                        0
                                        Да, не всё идеально в Си. Тот же Паскаль сильно бьёт по рукам за использования просто Pointer где не надо, а указатель на указатель вообще редкость(но бывает). Ещё можно отметить использование хака include вместо нормальной модульности. Ну что же, нет в мире совершенства.
                                          0
                                          А как паскаль бьет по рукам за Pointer?
                                        +15
                                        Вы из одной крайности в другую бросаетесь, на самом деле синтаксиса «си с классами» ревизии эдак C++99 было достаточно для всего, и он был весьма удобен и прост, в отличие от голого си, где даже нет RAII. Выбирать голый си вместо этого, увеличивая объем кода раза в 4 и увеличивая вероятности внесения ошибок это действительно как минимум странно.
                                        Вообще лучше иметь какую-то возможность, чем не иметь, вас же никто не заставляет всем этим пользоваться для любой задачи, в этом и прелесть, можете писать в стиле С, если так по вашему проще, а можете какие-нибудь лямбды с ренджами завернуть при случае, если это сократит код в 10 раз.
                                          +3
                                          Дак собственно и проблема что в «Си-с-классами», который сильно условно «объектно ориентированый» и не очень «метапрограмированный» пытаются натянуть еще больше объектно ориентированности и метапрограммируемости. Все косяки плюсов на которые кивают опоненты из разряда «агааа вот как криво реализована эта фича, множество ограничений и UB». Ну дак новые стандарты кривости не исправляют, максимум подпирают костылями, а плюсом втыкают еще грабли… 99 стандарт «жил» 12 лет. а 11 стандарт только 3 года, и отнюдь не из за того что за 3 года «придумали новые хорошие плюшки». Зачем это делать лично мне непонятно.

                                          Хороший пример, это собеседования… Когда вместо написания кода просят пояснить чем make_shared отличается от конструктора shared_ptr. Зачем forward, если есть move и прочее. Т.е. просят понимания ньюансов языка. При этом вопросы про ньюансы работы всяких «вызов виртуального метода в конструкторе» — никуда не ушли.

                                          +4

                                          Идеология C++ заключается в том, что "ты не платишь за то, что не используешь". Никто не заставляет писать все в стиле C++ 20, выбирайте по месту те механизмы из наличия, которые в данном случае с вашей точки зрения будут удобнее и нагляднее. А безумный лапшеобразный код одинаково легко пишется на любом более-менее развитом языке, это просто вопрос квалификации пишущего. Если человек неграмотный, то никакой Rust тут не спасёт.

                                            +5
                                            Так часто приходиться не самому писать, а чужой код читать. Когда сам писал, то сам не понимал чистых сишников. Потому, что без классов всё же хуже, чем с оными.
                                              +3
                                              Иногда все же платишь, просто включение флага C++17 может заметно замедлить компиляцию, как показывает этот пост.
                                                –1
                                                Так если не используешь, зачем включать этот флаг?
                                                  +1
                                                  ты не используешь, а библиотека которую ты подключил, его требует. Например fmt какой-нибудь (я знаю, там сейчас C++11 вроде минимум, но представьте что это какая-то другая либа). И хоба, даже если вы либу в одном файле не включили, все равно его компиляция замедляется (извращенства вроде выставления набора флагов для каждого файла не предлагать)
                                                    0
                                                    Ну это уже фактически «используешь», если библиотека собирается из исходников.
                                                    Хотя, кмк, собирать библиотеку отдельно, а потом уже линковать было бы неплохим решением.

                                                +1
                                                Идеология C++ заключается в том, что «ты не платишь за то, что не используешь».
                                                Но в итоге все равно платишь, например когда код стал компилироваться дольше, а IDE стала монструозно тяжелой и медленной, но ты не можешь взять старую, потому что в проекте кто-то где-то использовал новую фичу или либу с таковой.
                                                  –1
                                                  Если в проекте «кто-то использовал» что-то, то, соответственно, уже нельзя сказать, что это что-то «не используется», а раз это что-то используется, то за это надо платить, в том числе временем компиляции тех модулей, где это используется. Другое дело, что нужно думать, что стоит тащить в проект, а что нет, должно быть какое-то элементарное review кода любителей тащить в проект «новые либы с новыми фичами».
                                                    0
                                                    Плата начинается с момента выбора современной IDE, а есть в коде что-то новое или нет это уже не важно. Смысл был в том что использовать старые IDE уже затруднительно, особенно если работаешь над коллективным проектом, и не всегда есть возможность выбора.
                                                      0
                                                      Давайте отделять мух от котлет. Писать сколько угодно сложный код можно хоть в Notepad'е. Другое дело, что для повышения удобства этого процесса используют навороченные IDE, они действительно парсят ваш код для работы всяких IntelliSense, но они не инстанцируют шаблоны, не инлайнят код, не занимаются вычислениями всяких constexpr и не оптимизируют потом все это. Я хочу сказать, что с C99 кодом такая IDE будет работать вряд ли сильно быстрее, чем с C++20 кодом (имеется в виду сам процесс писанины, а не сборки).
                                                        +1
                                                        Я хочу сказать, что с C99 кодом такая IDE будет работать вряд ли сильно быстрее, чем с C++20 кодом
                                                        В этом то и дело. Даже если писать C99 код, необходимость поддержки IDE новых стандартов плюсов с кучей их фич ведет к ее монстеризации и замедлению. И с каждым новым стандартом это сказывается все сильнее и сильнее. При этом не всегда можно просто взять старую IDE, даже если пишешь на C99. Возвращаясь к изначальному утверждению, мы платим в том числе за то, что не используем, но за потенциальную возможность это использовать.

                                                        Всякие возможности рефакторинга и навигации существуют параллельно со всем этим.
                                                          +1

                                                          Я как раз таки сильно сомневаюсь, что "монстеризация и замедление" и "поддержка новых стандартов C++" так уж связаны. Вон новые версии Скайпа не поддерживают ни одного стандарта C++, а все равно еле ворочаются на средненьком ноутбуке пятилетней давности, потому что Электрон, вот это вот все :) Сейчас вообще тенденции в разработке ЛЮБОГО ПО направлены в сторону "монстеризации и замедления" (под лозунгом "ускорим разработку ценой скорости работы готового продукта", что, кстати, хорошо видно по некоторым комментариям к этой статье), а IDE просто следуют в общем тренде.

                                                            0
                                                            Конечно, не одними стандартами IDE жирнеют, но свою лепту они вносят однозначно. Особенно это касается поддержки всех этих новых фич IntelliSense'ом.
                                                          0
                                                          но они не инстанцируют шаблоны
                                                          не занимаются вычислениями всяких constexpr

                                                          Вообще-то это плохие IDE, если они этого не делают. Инстанциировать шаблоны и вычислять constexpr нужно для адекватного Intellisense (и на самом деле даже для адекватной раскраски кода).

                                                            0
                                                            Ну не знаю — не знаю. IDE именно с IntelliSense (типа Visual Studio) у меня сейчас под рукой нет, есть Qt Creator, он, конечно, выводит типы в случаях типа такого:

                                                            constexpr auto fn(int k)
                                                            {
                                                                return k + 1;
                                                            }
                                                            
                                                            constexpr auto var = fn(3);
                                                            


                                                            то есть при наведении мышки на var показывает, что тип у него int, но нет никаких признаков, что он пытается именно вычислять значение var.
                                                              0

                                                              Как насчёт чего-то вроде


                                                              std::conditional_t<fn(3) > 0, int, char> foo = 10;

                                                              ?


                                                              Ну и подсветить это красненьким, если выбранная ветка будет несовместима с инициализатором? Например, если вместо int написать std::string.

                                                                0
                                                                Ну вообще да, действительно вычисляет. Только скобочки нужны:

                                                                std::conditional_t<(fn(3) > 0), int, char> foo = 10;
                                                                


                                                                Так показывает int, если ">" заменить на "<", показывает signed char. Но опять же, IDE скорее всего делает это гораздо реже, чем компилятор — например, в случае с constexpr auto IDE вряд ли будет утруждать себя лишними вычислениями. То есть скорее всего принцип «ты не платишь за то, что не используешь» все же работает и здесь.
                                                  +1
                                                  Согласен, сознательное ограничение в языковых средствах имеет смысл.
                                                  +5
                                                  это свежевыдуманный синтаксис, потому что в стандарте C++ корутин нет

                                                  Это не свежевыдуманный синтаксис, а синтаксис из актуального Coroutines TS

                                                    +3
                                                    Это мой косяк, ща поправлю. В оригинале было написано «tentative syntax», каламбур прошёл мимо меня, не оглядываясь.
                                                    +4
                                                    Прочитав блогпост, я думаю, что все это притянуто за уши. Этот пример с пифагоровыми тройками привели, чтобы показать, что есть в ranges и как это можно использовать. Но кто-то подумал, что так нужно писать код с приходом нового стандарта?
                                                    А в реальном проекте как это всё будет? Отнюдь я относительно мало взаимодействую с плюсами по работе, но кмк, реальный проект с этим будет собираться не сильно медленнее, а при умеренном использовании даже будет более читабильно.
                                                    А пример из разряда, что будет если взять все известные паттерны и соединить их вместе.
                                                      +5
                                                      Понимаю, что оффтоп, но то же самое на Питончике делается совсем красиво, просто и наглядно:
                                                      >>> def triples():
                                                          z = 0
                                                          while True:
                                                              z += 1
                                                              for x in range(1, z):
                                                                  for y in range(x, z):
                                                                      if x*x + y*y == z*z:
                                                                          yield x, y, z
                                                      
                                                      >>> # Потестируем
                                                      >>> import time
                                                      >>> def test_triples(n):
                                                          t_beg = time.perf_counter()
                                                          first_n = [triple for _, triple in zip(range(n), triples())]
                                                          duration = time.perf_counter() - t_beg
                                                          print(*first_n, sep='\n')
                                                          print(f'{n} triples in {duration:0.4f} seconds')
                                                      
                                                      >>> test_triples(100)
                                                      (3, 4, 5)
                                                      (6, 8, 10)
                                                      (5, 12, 13)
                                                      (9, 12, 15)
                                                      (8, 15, 17)
                                                      (12, 16, 20)
                                                      ...
                                                      (65, 156, 169)
                                                      (119, 120, 169)
                                                      (26, 168, 170)
                                                      100 triples in 0.1836 seconds
                                                      
                                                        +2

                                                        Ну да, только программа на С++ работает в 180 раз быстрее. А если программа на С++ отрабатывает за час, скажем, то эквивалентный питончик будет работать 180 часов, если не дольше. Серебрянной пули нет и все такое.

                                                          +8
                                                          Хоба!

                                                          # triples.pyx
                                                          def triples():    
                                                              cdef int x = 0
                                                              cdef int y = 0
                                                              cdef int z = 0
                                                              (...)

                                                          import time
                                                          import pyximport; pyximport.install()
                                                          from triples import triples
                                                          
                                                          def test_triples(n):
                                                              (...)

                                                          Было: 100 triples in 0.1393 seconds
                                                          Стало: 100 triples in 0.0009 seconds

                                                          Cython конечно не везде прокатывает, но зачастую очень выручает.
                                                            +5

                                                            Ну да, но только Cython компилируется, притом С компилятором. Возможно, компиляция будет быстрее за счет того, что код транслируется в С, не в С++. Это если о времени компиляции говорить. А код, наверное, да, писать проще. Но я с Cython не работал, не знаю.


                                                            Я подозреваю, если писать что-то длиннее, чем такой скрипт, как в статье, вылезет куча проблем или подводных камней. Например, в С++ я могу сказать для каждой переменной: вот ты будь на стэке, а ты в куче. Могу вернуть из функции unique_ptr и не копировать результат (и памятью вручную не управлять). А в Cython, судя по документации, надо вызывать Сишные free и malloc для ручного управления памятью… Может быть, для большинства задач, где нужна производительность, в итоге проще сразу взять С++ (?).

                                                            +6
                                                            Теоретически да, но есть нюансы. Скорость работы программы определяется не только скоростью отработки элементарных операций. Если разрабу не приходится воевать с инструментом, если среда к нему дружелюбна, то у него остаются силы поразмыслить над алгоритмом, над красотой реализации, над гибкостью решения и прочими такими как-бы не очень важными вещами. Глядишь, и придумается у него, как O(N^3) превратить в какое-нибудь O(N^2), и тогда его питонский код сделает C++ного как щенка.

                                                            Например, взглянув на приведённую выше функцию, можно заметить, что каждый раз делать умножения z*z и x*x не нужно, и поэтому:
                                                            >>> def triples():
                                                                z = 0
                                                                while True:
                                                                    z += 1
                                                                    z2 = z*z
                                                                    for x in range(1, z):
                                                                        x2 = x*x
                                                                        for y in range(x, z):
                                                                            if x2 + y*y == z2:
                                                                                yield x, y, z
                                                            

                                                            В результате вместо «100 triples in 0.1836 seconds» получаем «100 triples in 0.0974 seconds». То есть уже медленнее не в 180 раз, а всего в 100. На такой простой фигне. Если присмотреться, можно заметить, что третий вложенный цикл лишний. Игрек можно вычислять сразу, вот так:
                                                            >>> def triples():
                                                                z = 0
                                                                while True:
                                                                    z += 1
                                                                    z2 = z*z
                                                                    for x in range(1, z):
                                                                        y2 = z2 - x*x
                                                                        y = int(round(y2**0.5))
                                                                        if y >= x and y*y == y2:
                                                                            yield x, y, z
                                                            

                                                            В результате мало того, что ещё в два раза разогнались («100 triples in 0.0498 seconds»), но и своё О() улучшили. На 10000 этот код у меня показывает такое: «10000 triples in 32.6228 seconds». Если сравнить с оригинальным алгоритмом на C++, наверняка Питон будет быстрее. Здесь можно возразить, что никто нам не мешает в C++ провернуть ту же каверзу. Но не всё так просто. В реальной жизни в реальных системах сишный код являет собой мегабайты жуткой лапши из классов, библиотек, шаблонов, выделений/освобождений памяти и прочего ужаса (сам этим занимался несколько лет кряду). Здесь не до всяких сопливых О(), здесь лишь бы не падало, и на том спасибо.
                                                              +7

                                                              Да, но с тем же успехом можно и С++-ный код переписать таким же способом и он снова будет в 180 раз быстрее. Но проблема в том, что часто и после переписывания код все равно медленный, ну просто потому, что быстрее он быть не может. И тогда С++ снова на коне. Если б все проблемы с производительностью можно было решить, подумав над алгоритмом, С++ бы давно и умер, наверное. Да и numpy бы не писали как обертку над С-шным кодом, и Cython вот никто бы не разрабатывал.


                                                              А, насчет последнего параграфа. Если руками памятью не управлять (то есть никаких new/delete/free/malloc в коде), то частота падений устремляется к нулю, мне кажется. Ну и ворнинги чистить, чтоб undefined behavior не было. Насчет легкости подключения библиотек—тут вы правы. Но, очевидно, все еще есть много задач, где скорость работы программ перевешивает этот недостаток. Ну и все ждут-не-дождутся модулей :-)

                                                                0
                                                                Но, очевидно, все еще есть много задач, где скорость работы программ перевешивает этот недостаток.
                                                                Совершенно верно. Поэтому C++ тоже любим. Просто для каждого типа задач свой инструмент. Делать быстрый расчёт на плюсах — в самый раз, инструментальщина — тоже, а юзать их для бизнес-логики, особенно в какой-нибудь динамичной предметной области, где семь пятниц на неделе — чистое безумие. Описанное в статье мне показалось попыткой затянуть в стандарт С++ то, что поможет решать те задачи, для которых С++ вообще напрочь не предназначен.
                                                                  0
                                                                  Да, но с тем же успехом можно и С++-ный код переписать таким же способом и он снова будет в 180 раз быстрее. Но проблема в том, что часто и после переписывания код все равно медленный, ну просто потому, что быстрее он быть не может. И тогда С++ снова на коне. Если б все проблемы с производительностью можно было решить, подумав над алгоритмом, С++ бы давно и умер, наверное. Да и numpy бы не писали как обертку над С-шным кодом, и Cython вот никто бы не разрабатывал.

                                                                  Да, только С++-ный код не получится так переписать, потому что время отведенное на задачу закончится.

                                                                    +4
                                                                    Да, только С++-ный код не получится так переписать, потому что время отведенное на задачу закончится.

                                                                    Да ладно, на питоне многие вещи писать быстрее, конечно, но не то чтобы прям сильно.
                                                                      +1

                                                                      Ну если считать время на разбор "что тут пришло в виде void** a[]" то очень даже сильно, имхо.


                                                                      Хотя есть компромиссы и получше, для меня это C#/Rust. Но и у питона ментальная модель для понимания кода в разы проще. https://habr.com/ru/company/jugru/blog/438260/#comment_19690430 например, например.

                                                                        +1

                                                                        Но void** a[] — это не C++.

                                                                          +2
                                                                          Когда в каждой компании свои умные указатели — это не сильно лучше. В хроме свои, у мозиллы свои, у `opencv` тоже свои… Это только из того, что я сам видел. «Да они все похожи, переучиться 5 наносек» — а вот не сказал бы.
                                                                            +1
                                                                            Свои умные указатели на свои динамические массивы своих строк. Сразу в нескольких библиотеках в проекте…
                                                                  +2
                                                                  В реальной жизни в реальных системах сишный код являет собой мегабайты жуткой лапши из классов, библиотек, шаблонов, выделений/освобождений памяти и прочего ужаса (сам этим занимался несколько лет кряду). Здесь не до всяких сопливых О(), здесь лишь бы не падало, и на том спасибо.


                                                                  Мне кажется вы несколько перегибаете. Переписывание именно мегабайтов кода на статически типизируемом языке (С++, например), как правило, не в пример легче переписывания кода на динамически типизируемом языке (Python). Особенно если на это накладывается условный NumPy с его вольными правилами бродкастинга.
                                                                  Проблемы с утечками, по большей части, решаются использованием инструментов типа valgrind, и соблюдением правил выделения/освобождения ресурсов.
                                                                    +2
                                                                    Можно еще ускориться на вычислении 10000 троек раз эдак в 15 )))

                                                                    from math import sqrt
                                                                    
                                                                    def triples():
                                                                        c = 4.
                                                                        while True:
                                                                            a, b = sqrt(c * 2. + 1.), c
                                                                            c += 1.
                                                                            сс = c * c
                                                                            while a < b:
                                                                                if a.is_integer():
                                                                                    yield int(a), int(b), int(c)
                                                                                b -= 1.
                                                                                a = sqrt(сс - b * b)
                                                                    
                                                                    0
                                                                    Ну да, только программа на С++ работает в 180 раз быстрее.
                                                                    Да, но не потому что ее код в примере выглядит уродским и трудночитаемым. То же самое и в обратную сторону.

                                                                    Можно же добавить в быстрый компилятор возможность писать красивый код.
                                                                    +5
                                                                    На рэнджах и итераторах раста.

                                                                    fn main() {
                                                                        (0..)
                                                                            .map(|z|
                                                                                (1..=z)
                                                                                    .map(move |x| (x..=z)
                                                                                    .map(move |y| (x,y,z))
                                                                                    .filter(move |(x,y,z)| x*x + y*y == z*z))
                                                                                    .flatten()
                                                                            )
                                                                            .flatten()
                                                                            .take(100)
                                                                            .for_each(|x| println!("{:?}", x));
                                                                    }
                                                                    
                                                                      +9
                                                                      Читабельность кода конечно просто зашкаливает :)

                                                                      То, что написато в оригинале на C поймет практически любой школьник. То, что вы привели на расте — они ни чем не лучше, чем на «C++20». И там и там одинаково хреново.
                                                                        +6

                                                                        Вы не мешайте в одну кучу задачи "вывести в цикле" или сделать итератор/генератор. Результат конечно похож, но это всё же не одно и тоже. Эти абстракции как раз для того, чтобы решать проблемы, описанные для C кода в статье.


                                                                        Уверен, что на большинстве языков (включая раст) — просто через for будет как на C. Но итератор или генератор зачастую полезнее.


                                                                        — добавлено --


                                                                        simple-reusable.cpp уже является аналогом, но не совсем: внутри он другой. На расте это комбинация итераторов рэнджей, в cpp — ручное создание итератора с нуля, даже с ручным сохранением состояния вроде.

                                                                          +3
                                                                          > Вы не мешайте в одну кучу задачи «вывести в цикле» или сделать итератор/генератор. Результат конечно похож, но это всё же не одно и тоже. Эти абстракции как раз для того, чтобы решать проблемы, описанные для C кода в статье.

                                                                          Конечно с этим я согласен! Задачи разные. Я лишь заметил, что когда в очередной раз приводят вроде как спасительный код на серебряной пуле он почему-то на деле оказывается совсем другого цвета. Ни чуть не лучше вот этого всего что плавает рядом.
                                                                            +3
                                                                            Признаться я не очень понял. Вы сами изначально сравнили некорректно, но в итоге сейчас написали, что всё равно вас не устраивает. Почему не устраивает или что не так — не объяснили. В целом это довольно обычная реакция на «непривычный синтаксис».
                                                                          0
                                                                          Вот то, что написано в оригинале на C, но на Rust, поймёт практически любой школьник:
                                                                          fn print_n_triples(n: u32) {
                                                                              let mut i = 0;
                                                                              for z in 1.. {
                                                                                  for x in 1..=z {
                                                                                      for y in x..=z {
                                                                                          if x*x + y*y == z*z {
                                                                                              println!("{}, {}, {}", x, y, z);
                                                                                              i += 1;
                                                                                              if i == n { return; }
                                                                                          }
                                                                                      }
                                                                                  }
                                                                              }
                                                                          }
                                                                          +2

                                                                          Начал писать свой вариант, прежде чем увидел, что уже написано… Ну да ладно, раз уж написал


                                                                          fn triples(n: u32) -> impl Iterator<Item = (u32, u32, u32)> {
                                                                              (0..n).flat_map(move |z| 
                                                                                  (1..z).flat_map(move |x| 
                                                                                      (x..z).map(move |y| 
                                                                                          (x, y, z)
                                                                                      )
                                                                                  )
                                                                              )
                                                                              .filter(|(x, y, z)| x*x + y*y == z*z)
                                                                          }
                                                                          
                                                                          fn main() {
                                                                              for triple in triples(10000).take(100) {
                                                                                  println!("{:?}", triple);
                                                                              }
                                                                          }

                                                                          https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=9ea7684067d9ef90e78308116afbc333


                                                                          Компилируется за 0.5 секунды, поиграть можно на годболте

                                                                            +1

                                                                            А для list comprehensions пропозалы в расте есть?


                                                                            Как HKT завезут, можно ж их вообще делать поверх любой монады.

                                                                              +2

                                                                              Не знаю, меня наоборот этот синтаксис очень раздражает.


                                                                              do x <- Just 1
                                                                                 y <- Just 2
                                                                                 return (x+y)

                                                                              Для меня лично выглядит намного приятнее, чем


                                                                              [ x + y | x <- Just 1, y <- Just 2 ]

                                                                              Про планы сделать что-то такое не слышал. Мне вообще кажется, что специальный синтаксис для этого не нужон.

                                                                                +2

                                                                                Ну, я, если честно, включал эти monad comprehensions, наверное, пару раз за всю жизнь. Последний раз — когда я работал со списками, но мне нужна была другая семантика для guard (бектрекал там что-то), так что да, это не сказать чтобы особо часто нужная фича.

                                                                              +1

                                                                              Прошу прощения, оптимизатор выкинул всё нафиг, потому что годболт компилирует в режиме библиотеки. Вот правильная ссылка: https://rust.godbolt.org/z/DiS1CC

                                                                                +4
                                                                                Простите за оффтоп, но меня очень забавляет, что вы меряетесь скоростью работы программ, запущенных на разных машинах и в разных окружениях… и никого это не смущает :)
                                                                                  +4

                                                                                  Я предлагаю мерить не скорость программ. Можно глянуть на ассемблер и прикинуть, что будет быстрее. Типа "ага, тут векторизовал, а тут нет, ну всё ясно".

                                                                                    +7
                                                                                    Для исходной программы на питоне это особенно хорошо подходит.
                                                                                  +2
                                                                                  Прошу прощения, но зачем вам ограничение до N? O_o
                                                                                  Тот же код, но с бесконечной последовательностью
                                                                                  fn triples() -> impl Iterator<Item = (u32, u32, u32)> {
                                                                                      (0..).flat_map(move |z| 
                                                                                          (1..z).flat_map(move |x| 
                                                                                              (x..z).map(move |y| 
                                                                                                  (x, y, z)
                                                                                              )
                                                                                          )
                                                                                      )
                                                                                      .filter(|(x, y, z)| x*x + y*y == z*z)
                                                                                  }
                                                                                  
                                                                                  fn main() {
                                                                                      for triple in triples().take(100) {
                                                                                          println!("{:?}", triple);
                                                                                      }
                                                                                  }

                                                                                    0

                                                                                    Мне показалось, что это имеет смысл. Хотя можно убрать, конечно.

                                                                                  0
                                                                                  Не так уродливо в примере из статьи, но совершенно не self explanatory, если человек не знаком с rust и map/filter подходом. Не говорю, что это плохо, возможно это лучший компромисс между универсально, быстро, надежно и читаемо. Может быть, стоит инвестировать в изучение таких патернов ради того, что бы избавиться от кучи более серьезных проблем.
                                                                                  Школьник и правда не поймёт, но должен ли?
                                                                                  +2
                                                                                  Раз пошла такая пьянка, то Питон меня опять не убедил, даже скорее разочаровал своим синтакисисом. То же на C#

                                                                                  using System;
                                                                                  using System.Collections.Generic;
                                                                                  using System.Diagnostics;
                                                                                  using System.Linq;
                                                                                  
                                                                                  namespace Triples
                                                                                  {
                                                                                      class Program
                                                                                      {
                                                                                          static IEnumerable<(int, int, int)> Triples()
                                                                                          {
                                                                                              var z = 0;
                                                                                              while (true)
                                                                                              {
                                                                                                  ++z;
                                                                                                  for (int x = 1; x < z; x++)
                                                                                                  for (int y = x; y < z; y++)
                                                                                                      if (x * x + y * y == z * z)
                                                                                                          yield return (x, y, z);            
                                                                                              }
                                                                                          }
                                                                                          
                                                                                          private static void TestTriples(int n)
                                                                                          {
                                                                                              var sw = new Stopwatch();
                                                                                              sw.Start();
                                                                                              var firstN = Triples().Take(n).ToList();
                                                                                              sw.Stop();
                                                                                              firstN.ForEach(i => Console.WriteLine(i));
                                                                                              Console.WriteLine($"{n} triples in {sw.ElapsedMilliseconds} milliseconds");
                                                                                          }
                                                                                          
                                                                                          static void Main(string[] args)
                                                                                          {
                                                                                              TestTriples(100);
                                                                                          }
                                                                                      }
                                                                                  }

                                                                                  ...
                                                                                  (96, 128, 160)
                                                                                  (36, 160, 164)
                                                                                  (99, 132, 165)
                                                                                  (65, 156, 169)
                                                                                  (119, 120, 169)
                                                                                  (26, 168, 170)
                                                                                  100 triples in 6 milliseconds


                                                                                  А вот эта конструкция что делает? Спрашиваю потому что сходу не разобрал, а гуглить по скобкам бесполезно )
                                                                                  first_n = [triple for _, triple in zip(range(n), triples())]
                                                                                  Уж не ленива ли она? Тогда что мы меряли?
                                                                                    +2
                                                                                    А вот эта конструкция что делает? Спрашиваю потому что сходу не разобрал, а гуглить по скобкам бесполезно )…
                                                                                    Уж не ленива ли она? Тогда что мы меряли?
                                                                                    Нет, не ленивая. Честно достаёт n элементов из генератора и складывает в массив. Полный аналог «Triples().Take(n).ToList();».
                                                                                    Механика достаточно простая. Функция zip создаёт итерируемый объект, сцепляющий две последовательности. Здесь мы сцепляем range(n), тупо последовательность n чисел с нескончаемой последовательностью triples(). Когда одна из сцепливаемых последовательностей заканчивается, zip финиширует. То, что выдаёт range(n), нам не интересно, и мы складываем это в мусорную переменную "_".
                                                                                    То же самое сделал бы такой код:
                                                                                    first_n = []
                                                                                    gen = triples()
                                                                                    for _ in range(n):
                                                                                        first_n.append(next(gen))
                                                                                    

                                                                                    Или такой:
                                                                                    import itertools
                                                                                    first_n = [triple for triple in itertools.islice(triples(), 0, 10)]
                                                                                    

                                                                                      0
                                                                                      Ну, раз на до диезе музыка пошла, то вот и на фа диезе

                                                                                      open System
                                                                                      open System.Diagnostics
                                                                                      
                                                                                      let triple = 
                                                                                          seq {
                                                                                          let mutable z  =  0
                                                                                          while true do
                                                                                              z <- z + 1
                                                                                              for x in 1..z-1 do
                                                                                                  for y in x..z-1 do
                                                                                                      if x * x + y * y = z * z then
                                                                                                          yield (x, y, z)            
                                                                                          }
                                                                                      
                                                                                      let gen_n_triples N = 
                                                                                          let sw = Stopwatch()
                                                                                          sw.Start()
                                                                                          let pifa = triple |> Seq.take N |> Seq.toList
                                                                                          sw.Stop()
                                                                                      
                                                                                          let pri_tri t = printfn "%A" t
                                                                                          List.iter pri_tri pifa.[..5]
                                                                                          printfn ". . . . ."
                                                                                          List.iter pri_tri pifa.[N-5..]
                                                                                      
                                                                                          printfn "\n\t%d triples in %O milliseconds (%O ticks)\n" N sw.ElapsedMilliseconds sw.ElapsedTicks
                                                                                      
                                                                                      [<EntryPoint>]
                                                                                      let main argv = 
                                                                                          gen_n_triples 100
                                                                                          0 
                                                                                      

                                                                                      (3, 4, 5)
                                                                                      (6, 8, 10)
                                                                                      (5, 12, 13)
                                                                                      (9, 12, 15)
                                                                                      (8, 15, 17)
                                                                                      (12, 16, 20)
                                                                                      . . . . .
                                                                                      (36, 160, 164)
                                                                                      (99, 132, 165)
                                                                                      (65, 156, 169)
                                                                                      (119, 120, 169)
                                                                                      (26, 168, 170)

                                                                                      100 triples in 7 milliseconds (25844 ticks)


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

                                                                                      А, мы тут языками соревнуемся?


                                                                                      module Main where
                                                                                      
                                                                                      pytha :: [(Int, Int, Int)]
                                                                                      pytha = [(x, y, z)
                                                                                              | z <- [0..]
                                                                                              , x <- [1..z]
                                                                                              , y <- [x..z]
                                                                                              , x * x + y * y == z * z
                                                                                              ]
                                                                                      
                                                                                      main :: IO ()
                                                                                      main = print $ take 100 pytha

                                                                                      Тайминги:


                                                                                        INIT    time    0.000s  (  0.000s elapsed)
                                                                                        MUT     time    0.000s  (  0.002s elapsed)
                                                                                        GC      time    0.000s  (  0.000s elapsed)
                                                                                        EXIT    time    0.000s  (  0.008s elapsed)
                                                                                        Total   time    0.000s  (  0.010s elapsed)

                                                                                      Т. е. программа за собой подчищает дольше, чем выполняется. И работает, кстати, в константной памяти, жрёт 72 килобайта. Мне даже думать не нужно было о том, чтобы так писать. yieldы какие-то...


                                                                                      А, код на питоне выполняется 0.13 с.

                                                                                        +3
                                                                                        Если судить по вашим увлечениям, то это, должно быть Хаскель?

                                                                                        Однако, этот код заставил меня задуматься над тем, что может стоит глянуть в сторону Хаскеля и изучить его. Спасибо за приведённый пример. ;)
                                                                                          +3

                                                                                          Да, вы верно поняли.


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

                                                                                          0

                                                                                          На windows компиляция и линковка haskell выглядят удручающе долго. Насколько я понимаю, все статически линкуется в exe файл, с размером в несколько мегабайт. Это чисто windows проблемы? Данные проблемы можно купировать флагами компиляции/линковки?


                                                                                          P.s. если я не ошибаюсь, то haskell полностью рассахаривает take 100 pytha в список троек, так как они вычислимы на этапе компиляции. Получается в примере посчитана производительность операции print.

                                                                                            +3
                                                                                            На windows компиляция и линковка haskell выглядят удручающе долго. Насколько я понимаю, все статически линкуется в exe файл, с размером в несколько мегабайт.

                                                                                            Ну что вы начинаете, нормально же общались!


                                                                                            А если серьёзно, то да, линковка в хаскеле (в ghc, конкретнее) — больная тема, там худшее и от мира статической, и от мира динамической линковки.


                                                                                            Собирается у меня на линуксе быстро, но файл да, большой, 17 метров.


                                                                                            P.s. если я не ошибаюсь, то haskell полностью рассахаривает take 100 pytha в список троек, так как они вычислимы на этапе компиляции. Получается в примере посчитана производительность операции print.

                                                                                            Хорошее замечание!


                                                                                            Сделал, чтобы оно читало из файла вместо хардкода — те же 0.002 секунды.


                                                                                            Дополнительно взял и побенчмаркал конкретно функцию вычисления criterion'ом, чтобы не измерять скорость print и readFile — получил


                                                                                            benchmarking pytha/100
                                                                                            time                 968.8 ns   (966.9 ns .. 971.5 ns)
                                                                                                                 1.000 R²   (1.000 R² .. 1.000 R²)
                                                                                            mean                 968.9 ns   (967.2 ns .. 972.2 ns)
                                                                                            std dev              7.781 ns   (4.271 ns .. 13.40 ns)
                                                                                              0
                                                                                              возможно на windows готовый бинарник проверяет антивирус, при компиляции go такое замечал
                                                                                              +1
                                                                                              А если 100 забить не константой, а подавать на вход программы? А то, как тут уже сказали, есть подозрение, что оно вычисляет на этапе компиляции.
                                                                                                0

                                                                                                Ничего не меняется, ответил выше.

                                                                                                  0
                                                                                                  Впечатляет, конечно. Даже стало жаль, что Хаскель для моих задач совсем ни в жилу.
                                                                                                    +1

                                                                                                    А что у вас за задачи? Просто Хаскель неплохо подходит для гораздо большего количества областей, чем принято считать.

                                                                                                      0
                                                                                                      Формочки, кнопочки и всякая прочая скучнейшая муть.
                                                                                                        0

                                                                                                        Про Хаскель не скажу, а на F# делают это всё. Плюс, реактивная модель неплохо ложится на иммутабельность и прочую функциональщину.

                                                                                                          0
                                                                                                          У нас это всё делают на 1С :))
                                                                                                0
                                                                                                А что нужно подчищать перед выходом такого, что операционка не подчистит?
                                                                                                  +1

                                                                                                  Ну, например, прогнать GC, вызвать финализаторы.


                                                                                                  В данном конкретном случае это что-то, связанное с многопоточностью, ибо если линковать с однопоточным рантаймом, то время в EXIT становится равным нулю.

                                                                                                  0
                                                                                                  Блин, как же это красиво на хаскеле! Ничего лишнего! Спасибо!
                                                                                                +6
                                                                                                Про время компиляции вот прям в точечку, вместо того чтобы сосредоточиться на действительно важном, они делают «интересное». С другой стороны использовать все эти головоломки, как и любые другие инструменты языка, нужно именно тогда, когда это действительно необходимо, а не потому что можем. Возможно пример с тройками как раз оказался из таких, и надо было привести задачу, которая без ренжей занимает 100 строк, а с ренжами — 50 и выглядит менее запутанно.
                                                                                                  +7
                                                                                                  Здравый смысл никто не отменял. Пример с тройками был выбран, чтобы было во второй половине поста рассказать о всех изменениях. Конечно так не должен выглядеть код в продакшене. Наглядней было бы для каждой возможности привести какой-то кусок кода и показать как повысилась его читабельность благодаря новым функциям. А тут какой-то совершенно противоположный подход.
                                                                                                    +2
                                                                                                    Друзья, простите за нескромный вопрос — почему изначальную проблему повторного использования нельзя решать обычным callback'ом? Сгенерировал тройку, дернул вызов — что там будет делаться — код генерации троек не интересует.
                                                                                                      +3

                                                                                                      Как написали выше, пифагоровы тройки — не очень удачный пример. Вообще же вы подняли старую проблему push vs pull iteration. push-iteration, которая на коллбэках, делается проще — но гораздо хуже ведёт себя в композиции.

                                                                                                        +3
                                                                                                        Как бы да, но с другой стороны представим, что мне нужен список троек. Значит мне надо создать пустой список, написать колбек, который добавляет тройку в этот список, дернуть генератор, передав ему коллбек (и список, возможно).

                                                                                                        Хорошо что сейчас в языке уже есть лямбды с замыканиями, поэтому не надо создавать отдельную функцию void list_callback(void *arg); но все равно, логика приложения размазывается по коллбекам. И в вырожденном случае начинает напоминать классическое асинхронное приложение, где коллбеки ездят на коллбеках и коллбеками погоняют. Кстати, async/await и придумали для того, что бы держать логику в одном месте, а не в пяти коллбеках.

                                                                                                        Поэтому да, вариант с корутиной из поста был бы лучше.
                                                                                                          +2
                                                                                                          Как бы да, но с другой стороны представим, что мне нужен список троек. Значит мне надо создать пустой список, написать колбек, который добавляет тройку в этот список, дернуть генератор, передав ему коллбек (и список, возможно).

                                                                                                          Так и на здоровье — обернем создание списка, вызов итератора в одну функцию/процедуру, пусть будет callback с context'ом или лямбда — кому как нравится.
                                                                                                          Код получится простой, читаемый.

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

                                                                                                          Не совсем. Если это одна функция с лямбдой, в которую все завернуто, то никакого размазывания нет. Четко видно, к чему лямбда относится.
                                                                                                            +1
                                                                                                            Так и на здоровье — обернем создание списка, вызов итератора в одну функцию/процедуру, пусть будет callback с context'ом или лямбда — кому как нравится.
                                                                                                            Код получится простой, читаемый.


                                                                                                            С лямбдой — да, можно запихнуть в одно место. С классическом коллбэком-функцией — у вас часть логики будет в коллбэке, часть — в основной функции. Уже начинаются проблемы с читаемостью.

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

                                                                                                            Тут с коллбэками-функциями будет вообще мрак, ибо логика теперь будет размазана по куче функций. Хотя, программисты на JS как-то справляются же.

                                                                                                            С коллбэками-лямбдами будет уже проще, но все равно, лямбда в лямбде уже попахивает извращением. Но опять же, в коде под Андроид, я видел еще и не такое.

                                                                                                            Я не то что говорю, что коллбэки — это абсолютное зло. Я сам ими пользуюсь. Просто однажды я пытался поддерживать драйвер в линуксе, который весь был сделан на коллбеках — и это был ад. Плюс, веселья еще добавляла активная многопоточность.
                                                                                                              +2
                                                                                                              С классическом коллбэком-функцией — у вас часть логики будет в коллбэке, часть — в основной функции. Уже начинаются проблемы с читаемостью.

                                                                                                              Такое может быть, но надо смотреть конкретный вариант. Если функции относительно небольшие и коллбэк описан рядом с основной функцией, то это не такая проблема.

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

                                                                                                              Не вижу проблемы, потому что первый CB будет работать с (условно) ComposeAnotherList() функцией, а чем она там внутри пользуется, есть у нее свой (второй) CB или нет — уже не наше дело. Для нас это просто готовый кирпич, который мы используем. Нам важен результат, который мы запросили из первого CB.

                                                                                                              Вполне допускаю, что есть ситуации, где это может быть запутанным, но умозрительно можно сделать читаемо и отлаживаемо.
                                                                                                                +2
                                                                                                                Такое может быть, но надо смотреть конкретный вариант. Если функции относительно небольшие и коллбэк описан рядом с основной функцией, то это не такая проблема.

                                                                                                                Проблема в трансформациях. Типичная история: взяли что-то, пофильтровали, смаппили, смаппили, вызвали пару нестандартных комбинаторов, собрали вектор.


                                                                                                                Делать это на коллбеках значит иметь ~10 уровней вложенности на ровном месте.


                                                                                                                Разница примерно такая же, как асинхронность на async/await и a.then(x => ...)

                                                                                                                  +1
                                                                                                                  Делать это на коллбеках значит иметь ~10 уровней вложенности на ровном месте.

                                                                                                                  Да, но каждый уровень относится к следующему как к готовому кирпичу, черному ящику. Какая разница, сколько там дальше будет уровней вложенности? (В разумных пределах, конечно, стек не бесконечный и так далее).
                                                                                                                    0

                                                                                                                    Потому что все это влияет на читаемость.


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


                                                                                                                    let a = [1_u64, 2, 3, 4, 5];
                                                                                                                    let result = a
                                                                                                                        .into_iter() // хотим поитерироваться
                                                                                                                        .map(|item| item.pow(2)) // возвести все числа в квадрат
                                                                                                                        .flat_map(|item| once(0).chain(once(item))) // перед каждым числом дописать ноль
                                                                                                                        .skip(1); // пропустить первый ноль (чтобы нули были между числами, а не перед каждым)

                                                                                                                    Ссылка на плейграунд


                                                                                                                    Предлагаете делать 4 вложенных коллбеков? Или развернуть цепочку вычислений в императивный цикл, где мы в одном месте играем, в другом нет, а третьим рыбу заворачиваем?

                                                                                                                      0
                                                                                                                      Потому что все это влияет на читаемость.

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

                                                                                                                      Это, пожалуй, дело вкуса. Не далее чем год назад имел спор именно по поводу читаемости stream-стиля, не хочется повторять его по тем же аргументам.

                                                                                                                      Предлагаете делать 4 вложенных коллбеков?

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

                                                                                                                      В данном случае у вас будет один CB, имеющий доступ к входному и выходному спискам — для каждого элемента — возведение в квадрат, добавление к выходному списку и добавление ноля (или добавление в начале).
                                                                                                                      Всё, по окончании итерации — удаление лишнего ноля.
                                                                                                                        0
                                                                                                                        Совершенно необязательно. Никто же не мешает выносить «скучный» технический код, разбавляющий общий смысл деталями, в отдельную функцию.

                                                                                                                        Да нет. я хочу все перед глазами видеть. Вносить только какие-то законченые куски кода, особенно если они довольно большие. Тут ничего большого нет.


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

                                                                                                                        Потому что вводить миллион переменных и раздувать код в 3 раза тут совершенно не нужно.


                                                                                                                        В данном случае у вас будет один CB, имеющий доступ к входному и выходному спискам — для каждого элемента — возведение в квадрат, добавление к выходному списку и добавление ноля (или добавление в начале).
                                                                                                                        Всё, по окончании итерации — удаление лишнего ноля.

                                                                                                                        А давайте пример, сравним, так сказать, подходы.




                                                                                                                        К слову про итераторы, ниже верно написали: в моем случае map реализовано в одной библиотеке, filter_map в другой, а skip в третьей. И все они собраны вместе благодаря общему интерфейсу. Как это будет работать в случае коллбеков, где авторы все по-разному сделают не представляю.


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

                                                                                                                          0
                                                                                                                          Да нет. я хочу все перед глазами видеть. Вносить только какие-то законченые куски кода, особенно если они довольно большие. Тут ничего большого нет.

                                                                                                                          Зачем, если можно сделать из отдельных блоков, каждый из которых — закончен и может гипотетически использоваться отдельно? Вам удобнее писать в stream стиле — пожалуйста. По мне, так читается не очень хорошо.

                                                                                                                          Вообще, интересный маятник с течением времени получается. От разбиения и сокрытия всего чего только можно и обратно к «давайте весь код будет здесь, одним куском, так нагляднее, но со stream'ами, чтобы не размазывать».

                                                                                                                          Потому что вводить миллион переменных и раздувать код в 3 раза тут совершенно не нужно.

                                                                                                                          Так не раздувайте. Вот у вас готовый push итератор, вот ваш custom коллбэк.

                                                                                                                          А давайте пример, сравним, так сказать, подходы.

                                                                                                                          В итоге получится, что ваш пример тоже недостаточно сложный, чтобы нехорошесть коллбэков (лямбд) была видна и далее будут дополнения «а если нам надо так, а если еще вот так и вот так». Но хорошо, давайте в псевдо-коде:
                                                                                                                          func buildSparseList(in inList) someListType
                                                                                                                          {
                                                                                                                            someListType outList;
                                                                                                                            new(outList);
                                                                                                                            iterateList(inList, (listItem)=>[outList.Add(listItem.pow(2)); outList.Add(0)]);
                                                                                                                            outList.trunc(outList.Capacity - 1);
                                                                                                                            return outList;  
                                                                                                                          }
                                                                                                                          

                                                                                                                          iterateList внутри — просто foreach с вызовом
                                                                                                                          listItem=>[] — лямбда, которая захватывает внешний контекст, включая outList. Если на чистых коллбэках — будет указатель на коллбэк и переменная контекста с cast'ом, как в старом С.

                                                                                                                          Я не говорю, заметьте, что ваш способ чем-то ужасен. Я говорю, что старый не так плох. Я бы мог писать и в потоковом стиле, и в императивном, но императивный мне нравится больше чисто субъективно.
                                                                                                                            0

                                                                                                                            Старый способ плох. Во-первых вы используете тот факт, что тип переменной один и тот же. Если у нас [1,2,3].map(|i| i as f64).map(|double| double.to_string()) то сделать так не выйдет — раз
                                                                                                                            У вас аллокации на ровном месте — два.
                                                                                                                            Куча изменяемого состояния, практически каждая строка мутирует переменные — три.


                                                                                                                            Зачем, если можно сделать из отдельных блоков, каждый из которых — закончен и может гипотетически использоваться отдельно? Вам удобнее писать в stream стиле — пожалуйста. По мне, так читается не очень хорошо.

                                                                                                                            Не знаю, кто скажет, что второй вариант читается лучше. Если есть такие, объясните, пожалуйста, как так получилось. Мне было бы крайне любопытно.

                                                                                                          +5

                                                                                                          Потому что изначальный вопрос шире. Давайте переиспользуем комментарий Александра Есилевича из моего фейсбука:


                                                                                                          "Проблема критикующих ranges в том, что они плохо понимают зачем это все создавалось и заменой чего эти ranges являются. А проблема продвигающих ranges в том, что они не могут правильно донести все это. Вот этот пример Ниблера делает все еще хуже, потому что показывает использование ranges для решения какой-то абстрактной задачи, которую можно решить тысячами способов, а о достоинствах и недостатках каждого способа можно дискутировать до бесконечности.


                                                                                                          В реальности ranges — это замена итераторам, которая позволяет определять свои собственные итераторы в несколько строк кода, вместо нескольких десятков строк кода на голом C++ или десятка строк с использованием Boost.Iterator. Для того, чтобы понять полезность ranges, надо сравнить код, определяющий свои кастомные итераторы, с аналогичным кодом на ranges. Кто вспомнит, когда в последний раз писал свои итераторы на голом C++? Я пожалуй в последний раз так делал никогда, только с использованием Boost.Iterator.


                                                                                                          Вот очень легко формулируемая задача, которая решается в пару строк на ranges, или в десяток строк на Boost.Iterator, а на голом C++ ее вообще обычно не решают никак, потому что сильно сложно.


                                                                                                          Задача формулируется так. Есть класс, содержащий внутри коллекцию std::unique_ptr. Надо выпихнуть наружу возможность пользователям класса перебирать объекты, содержащиеся в коллекции, но без возможности изменить их. Т. е. вернуть пару итераторов, у которых value_type — это "const SomeObject*" (можно и просто "const SomeObject", в зависимости от конкретной задачи).

                                                                                                          В реальности никто эту задачу обычно по-правильному на голом C++ не решает. Я обычно использую Boost.Iterator, если есть возможность, но это очень напрягает. С ranges все элементарно и просто."

                                                                                                            +1
                                                                                                            > Кто вспомнит, когда в последний раз писал свои итераторы на голом C++? Я пожалуй в последний раз так делал никогда, только с использованием Boost.Iterator.

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

                                                                                                            > Вот очень легко формулируемая задача, которая решается в пару строк на ranges, или в десяток строк на Boost.Iterator, а на голом C++ ее вообще обычно не решают никак, потому что сильно сложно.

                                                                                                            Приведенный ниже код случайно не устроит отца русской демократии :-? Вот только что поняв задачу буквально накидал за 10ть минут:
                                                                                                            #include <iostream>
                                                                                                            #include <memory>
                                                                                                            #include <list>
                                                                                                            
                                                                                                            struct Item
                                                                                                            {
                                                                                                                Item(const Item&) = delete;
                                                                                                                Item &operator = (Item&) = delete;
                                                                                                            
                                                                                                                explicit Item(int value) : value(value) {}
                                                                                                            
                                                                                                                int value;
                                                                                                            };
                                                                                                            
                                                                                                            inline std::ostream &operator << (std::ostream &os, const Item &item)
                                                                                                            {
                                                                                                                return os << std::dec << item.value;
                                                                                                            }
                                                                                                            
                                                                                                            template <class T>
                                                                                                            class Storage {
                                                                                                            private :
                                                                                                                using PtrList = std::list<std::unique_ptr<T>>;
                                                                                                            
                                                                                                            public :
                                                                                                                void addItem(std::unique_ptr<T> item)
                                                                                                                {
                                                                                                                    items_.push_back(std::move(item));
                                                                                                                }
                                                                                                            
                                                                                                                class const_iterator {
                                                                                                                public :
                                                                                                                    explicit const_iterator(typename PtrList::const_iterator it) : it_(it) {}
                                                                                                            
                                                                                                                    const T& operator * () const noexcept
                                                                                                                    {
                                                                                                                        return *it_->get();
                                                                                                                    }
                                                                                                            
                                                                                                                    bool operator != (const const_iterator &src) const noexcept
                                                                                                                    {
                                                                                                                        return it_ != src.it_;
                                                                                                                    }
                                                                                                            
                                                                                                                    const_iterator operator ++ () noexcept
                                                                                                                    {
                                                                                                                        it_++;
                                                                                                                        return *this;
                                                                                                                    }
                                                                                                            
                                                                                                                private :
                                                                                                                    typename PtrList::const_iterator it_;
                                                                                                                };
                                                                                                            
                                                                                                                const_iterator begin() const noexcept
                                                                                                                {
                                                                                                                    return const_iterator(items_.begin());
                                                                                                                }
                                                                                                            
                                                                                                                const_iterator end() const noexcept
                                                                                                                {
                                                                                                                    return const_iterator(items_.end());
                                                                                                                }
                                                                                                            
                                                                                                            private :
                                                                                                                PtrList items_;
                                                                                                            };
                                                                                                            
                                                                                                            int main()
                                                                                                            {
                                                                                                                Storage<Item> storage;
                                                                                                            
                                                                                                                for (int i = 0; i < 10; i++) {
                                                                                                                    auto item = std::make_unique<Item>(i);
                                                                                                                    storage.addItem(std::move(item));
                                                                                                                }
                                                                                                            
                                                                                                                for (const auto &item : storage) {
                                                                                                                    std::cout << "Item " << item << std::endl;
                                                                                                                }
                                                                                                            
                                                                                                                return 0;
                                                                                                            }
                                                                                                            

                                                                                                              –1

                                                                                                              А вот такое накидал Александр (в том же треде на ФБ) мгновенно. Не нужно знать, как писать кастомные итераторы. И в отличие от публичного наследования итератора такой код нельзя хакнуть и поменять элементы — а это между прочим условие задачи.


                                                                                                              #include <vector>
                                                                                                              #include <memory>
                                                                                                              #include <range/v3/view/transform.hpp>
                                                                                                              
                                                                                                              class MyObject {
                                                                                                              public:
                                                                                                                  int x = 20;
                                                                                                              };
                                                                                                              
                                                                                                              class MyCollection {
                                                                                                              public:
                                                                                                                  auto objects() const {
                                                                                                                      auto fn = [](const std::unique_ptr<MyObject> & ptr) { return const_cast<const MyObject*>(ptr.get()); };
                                                                                                                      return objects_ | ranges::view::transform(fn);
                                                                                                                  }
                                                                                                              
                                                                                                              private:
                                                                                                                  std::vector<std::unique_ptr<MyObject>> objects_;
                                                                                                              };
                                                                                                              
                                                                                                              int main() {
                                                                                                                  const MyCollection col;
                                                                                                                  for (auto && obj : col.objects()) {
                                                                                                                      int a = obj->x;
                                                                                                                  }
                                                                                                              }
                                                                                                                +1
                                                                                                                > И в отличие от публичного наследования итератора такой код нельзя хакнуть и поменять элементы — а это между прочим условие задачи.

                                                                                                                Не совсем если честно понял это замечание. Каким образом в приведенном мною случае вы сможете поменять возвращенный объект в коллекции :-?

                                                                                                                PS: Естественно не прибегая к const_cast-у т.к. это грязный хак по-определению. Решается административными средствами в процессе code review «с последующим лишением премии».
                                                                                                                  +2
                                                                                                                  return objects_ | ranges::view::transform(fn);

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

                                                                                                                    +1
                                                                                                                    Да тут собственно хоть используй оператор, хоть не используй, все равно ничего не понятно будет, если читающий не понимает что такое ranges и как они используются. От того, что будет «return ranges::view::transform(objects_, fn);» ничего принципиально не изменится. С итераторами на самом деле все точно так же: не понимая концепции итераторов код понять практически невозможно.
                                                                                                                      +1

                                                                                                                      Да не, аналогом было бы скорее что-то вроде


                                                                                                                      return  ranges::view::create_view(objects_, ranges::view::transform(fn))

                                                                                                                      и так действительно понятнее.

                                                                                                                    0

                                                                                                                    Версия ianzag выглядит намного более простой и прямолинейной по моему личному мнению. В ней мало что происходит за сценой, а объем не настолько уж велик. Есть подозрение, что и по времени компиляции намного выигрывает.
                                                                                                                    В C# реализация перечислений по сути сводится к одному интерфейсу, как по мне это идеальный баланс между "явностью" и затратами разработчика. Конечно в C++ сложнее такое реализовать, учитывая констатность, смарт-поинтеры и прочие особенности, но вышло откровенно не очень. Озвученный пример некоего Александра я понимаю с натяжкой, поскольку знаю задачу, за обозримое время не зная синтаксиса точно не смог бы восстановить задачу по реализации (хотя опыт позволяет читать код даже на незнакомых языках), а ведь на С++ (в основном в паре с Qt) пишу уже 8 лет.
                                                                                                                    P.S. Ни один человек не напечатает этот код за время, хоть сколько-нибудь близкое к "мгновенно".

                                                                                                                    +1
                                                                                                                    Ваш итератор не будет работать со стандартными алгоритмами. Вот такое не будет компилироваться:
                                                                                                                    auto res = std::find_if(storage.begin(), storage.end(), [](auto && ptr) { return ptr->x == 20; });
                                                                                                                    

                                                                                                                    А еще ваш итератор удовлетворяет критериям только ForwardIterator, но не BidirectionalIterator или RandomAccessIterator, т. е. будет работать только с частью алгоритмов (когда вы сделаете его пригодным для использования с алгоритмами), а с некоторыми алгоритмами будет работать медленнее, чем можно было бы.
                                                                                                                      +1

                                                                                                                      Я правильно понимаю, что ranges — это что-то типа слайса, итератор с возможностью индексированного доступа?

                                                                                                                        +4
                                                                                                                        range — это по сути пара итераторов, begin и end. Итератор в C++17 может сам по себе иметь индексированный доступ. Плюс есть еще sized range — это range, для которого еще определена операция size, для получения размера с константной сложностью.
                                                                                                                        0
                                                                                                                        > Ваш итератор не будет работать со стандартными алгоритмами.
                                                                                                                        > А еще ваш итератор удовлетворяет критериям только ForwardIterator, но не BidirectionalIterator или RandomAccessIterator, т. е. будет работать только с частью алгоритмов (когда вы сделаете его пригодным для использования с алгоритмами), а с некоторыми алгоритмами будет работать медленнее, чем можно было бы.

                                                                                                                        А он и не должен. Поскольку:

                                                                                                                        > Задача формулируется так. Есть класс, содержащий внутри коллекцию std::unique_ptr. Надо выпихнуть наружу возможность пользователям класса перебирать объекты, содержащиеся в коллекции, но без возможности изменить их.

                                                                                                                        Чему с моей точки зрения приведенный мною пример вполне удовлетворяет, разве нет?
                                                                                                                          0
                                                                                                                          Там вообще-то есть специальное уточнение: «Т. е. вернуть пару итераторов,...». Ваш класс не является итератором в терминологии STL. С таким же успехом можно было бы добавить метод «const SomeObject* object_at_index(size_t i) const» и считать, что задача выполнена.
                                                                                                                            0
                                                                                                                            > 0 Там вообще-то есть специальное уточнение: «Т. е. вернуть пару итераторов,...»

                                                                                                                            Не-не-не, господа, позвольте! Задача четко сформулирована в одном единственном предложении. Всякие «то-есть» или «ну может быть» и «было бы неплохо» — это уже за рамками ТЗ. Их можно учитывать а можно не учитывать. На усмотрение исполнителя.

                                                                                                                            Я сейчас, конечно же, не про итераторы и не про ренжи. Я сейчас сугубо про культуру постановки задачи :) Хотите, чтобы моторная лодка имела функцию вертикального взлета? Пожалуйста! Преодолевала звуковой барьер? Не вопрос! Укажите это явным образом в ТЗ. В противном случае она будет плавать но не более того.
                                                                                                                              0
                                                                                                                              Ну т. е. мы пришли к пониманию, что задача определения собственных итераторов даже для таких простых вещей, как добавление const, довольно трудна и занимает десятки строк кода?
                                                                                                                                0
                                                                                                                                Ну на самом деле само по себе количество строк кода — это не самый важный критерий. При прочих равных я бы предпочел двадцать строк кода но тупого и понятного основной массе одной, к которой без поллитры даже не подходить.
                                                                                                                                  0
                                                                                                                                  Так в том то и дело, что код итераторов — он только кажется тупым, а на самом деле он довольно сложен. Начать с того, что надо определить, каким требованиям будет соответствовать возвращаемый итератор. А после этого аккуратно реализовать все требуемые операции. В реальности это обычно встречается только в библиотеках, а в прикладном коде почти никогда, потому что сильно сложно и муторно. Ranges решают эту проблему. Использовать их для таких вещей в прикладном коде очень просто.
                                                                                                                                    0
                                                                                                                                    Возможно, возможно. Если ranges позволяет просто и наглядно решить эту проблему — это хорошо.

                                                                                                                                    Мы видимо рассматриваем проблему языка с разных колоколен. Я смотрю с сугубо практической стороны. Мне редко приходится на практике применять сложные языковые конструкции C++. Зато я видел тонны, просто тонны кода — как открытого так и в основном коммерческого — где разработчики имели очень, очень отдаленное представление как о культуре ООП так и о культуре С++ в частности. Когда std::auto_ptr<> является недостижимой вершиной а сокрытие данных в классе — назойливой мухой от которой все отмахиваются. Результат подхода легко предсказуем и всегда сбывается на практике. И потом вот это вот все «управляет атомной станцией». Некоторые вещи, кстати, почти без преувеличения.

                                                                                                                                    А вы про какие-то ренжи… Я понимаю желание сделать язык лучше. Но в ситуации, когда львиная доля реальных конечных пользователей языка не в состоянии принять элементарные идиомы которым уже буквально четверть века — какие там нафик ренжи…
                                                                                                                            0
                                                                                                                            Ну т. е., помимо формулировки задачи, вы же понимаете, что ваше решение приведет к тому, что рано или поздно пользователи вашего класса наткнуться на то, что хотя бы вот такой простой код не будет работать, хотя все ожидают, что он должен работать:
                                                                                                                            for (auto it = storage.begin(), end = storage.end(); it != end; it++) {
                                                                                                                            }
                                                                                                                            

                                                                                                                            А еще на вашем итераторе не будут работать такие базовые вещи, как например std::advance, что тоже вызовет мягко говоря удивление у пользователей.
                                                                                                                              0
                                                                                                                              > Ну т. е., помимо формулировки задачи, вы же понимаете,

                                                                                                                              Нет, товарищи, не понимаю и понимать не хочу. Категорически отказываюсь! Все геморрои всегда начинаются именно с этих слов. Нафик-нафик. Есть ТЗ — извольте в рамках договоренностей :)

                                                                                                                              > что тоже вызовет мягко говоря удивление у пользователей.

                                                                                                                              У пользователей всегда возникнет удивление. Как бы мы не старались. По другому и быть не может. А вот кому они выставят счет — это вопрос. Самый в общем то интересный и насущный. Если исполнитель будет «нувыпонимать» — ммм ну вы понимаете, кому его переадресует заказчик и каково будет исполнителю :)
                                                                                                                                +1
                                                                                                                                Тут нет заказчиков, исполнителей, ТЗ, и прочего. Тут есть обсуждение ranges и для чего они нужны. Я надеюсь теперь стало понятно, что имелось в изначальном тексте.
                                                                                                                                  0
                                                                                                                                  Нет, товарищи, не понимаю и понимать не хочу. Категорически отказываюсь! Все геморрои всегда начинаются именно с этих слов. Нафик-нафик. Есть ТЗ — извольте в рамках договоренностей :)

                                                                                                                                  Считайте, что с задачей справились, и это ЧТЗ на доработки, если так проще. Как выше сказали, тут нет каких-то ТЗ. Да и само ТЗ обычно формируется с двух сторон, продукт показывает, что он примерно хочет, а дальше ТЗ допиливается реализаторами, где что-то забыли, где что-то неправильно указали, и т.п.
                                                                                                                                    0
                                                                                                                                    КМК, вы все же не правы в своем отрицании :)
                                                                                                                                    Если уж придираться к словам в ТЗ, то там не написано, вывести объекты, а написано перебрать объекты. Способов перебрать объекты достаточно много и все они оперяются на концепты итераторов. Ваш же итератор, если быть честным, не удовлетворяет ни одному из концептов.
                                                                                                                          +5

                                                                                                                          У меня есть два генератора, один генерирует пифагоровы тройки, другой — какие-нибудь другие тройки. И пусть это всё счастье на коллбеках.


                                                                                                                          Как мне сделать генератор, совмещающий эти элементы как zip?


                                                                                                                          Как сделать генератор, строящий из списков [p0, p1, p2, ...] и [o0, o1, o2, ...] список [p0, o0, p1, o1, ...]?


                                                                                                                          Как ограничить количество генерируемых троек?


                                                                                                                          Если у меня ленивые списки, например, то мне об этом обо всём вообще даже думать не надо.

                                                                                                                          –3
                                                                                                                          > Например, мы здесь в Unity любим шутить
                                                                                                                          Ну да, смотря на качество этого лучшего движка в мире, как-то и не верется что там работают люди, умеющие программировать.
                                                                                                                            +4
                                                                                                                            Олег, а вот по поводу этого:
                                                                                                                            Его мгновенная реакция была вроде: «о, это какой-то старый код в стиле Си?». И я такой: «нет, в точности до наоборот!».

                                                                                                                            Объективно, недавнему студенту сложнее принять код на С, или навороченный код на С++?
                                                                                                                            Если дать те примеры, которые ты привёл в статье, студенту, какой из них он быстрее поймёт?

                                                                                                                            Мне тоже код на современных С++ кажется диким невоспринимаемым ужасом, но, может, они другие?
                                                                                                                            Может, «поколение зеро» воспринимает лучше именно такие абстракции, но не понимает С (и соответственно, не понимает, что такое «процессор», как он работает)?
                                                                                                                              +3
                                                                                                                              Мы уже дизайним Kotlin++
                                                                                                                                –2
                                                                                                                                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:


                                                                                                                                Чтобы читающих ценителей прекрасного случайно не стошнило от неожиданной встречи с goto можно:
                                                                                                                                1. Не стесняться проверять i в условиях выхода из циклов либо
                                                                                                                                2. Не стесняться завести более универсальную переменную done, и в циклах проверять уже её
                                                                                                                                  +4

                                                                                                                                  Можно просто return написать.

                                                                                                                                    0
                                                                                                                                    Код ошибки не забудьте только, там у main возвращаемое значение типа int.
                                                                                                                                    Время выполнения программы, правда, еще не посчитается.
                                                                                                                                    Но ничего, зато Вы return напишете!
                                                                                                                                      +3

                                                                                                                                      Мб имелось в виду, засунуть цикл в функцию и из неё уже сделать return?

                                                                                                                                        +3
                                                                                                                                        Ну посчитайте время перед return.
                                                                                                                                        Или вообще пусть Вам время считает объект в стиле RAII: само посчитается, как бы и где бы не вышли из функции.
                                                                                                                                        Раз уж на c++ пишем.

                                                                                                                                        Или как уже написали: обернуть циклы в функцию, а время считать вне её.
                                                                                                                                          +1

                                                                                                                                          Если бы в плюсах были удобные лямбды, ими чаще бы пользовались, например:


                                                                                                                                          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)
                                                                                                                                                                  return;
                                                                                                                                                          }
                                                                                                                                          }();

                                                                                                                                          Хотя это все равно грязновато, вариант с итераторами сильно удобнее.

                                                                                                                                      +3
                                                                                                                                      (это предварительный синтаксис, потому что в стандарте C++ корутин нет):
                                                                                                                                      Зато есть старый добрый вычисляемый goto switch:
                                                                                                                                      #include <iostream>
                                                                                                                                      #include <tuple>
                                                                                                                                      
                                                                                                                                      #define CO_BEGIN \
                                                                                                                                      	{ \
                                                                                                                                      		switch (lineno) \
                                                                                                                                      		{ \
                                                                                                                                      		case 0: \
                                                                                                                                      	//
                                                                                                                                      #define CO_END }}
                                                                                                                                      #define CO_YIELD(value) \
                                                                                                                                      	do { \
                                                                                                                                      		lineno = __LINE__; \
                                                                                                                                      		return (value); \
                                                                                                                                      		case __LINE__:; \
                                                                                                                                      	} while (false) \
                                                                                                                                      	//
                                                                                                                                      
                                                                                                                                      class PytripleGenerator
                                                                                                                                      {
                                                                                                                                      public:
                                                                                                                                      	std::tuple<int, int, int> next()
                                                                                                                                      	CO_BEGIN
                                                                                                                                      		for (z = 1; ; ++z)
                                                                                                                                      			for (x = 1; x <= z; ++x)
                                                                                                                                      				for (y = x; y <= z; ++y)
                                                                                                                                      					if (x*x + y*y == z*z)
                                                                                                                                      						CO_YIELD(std::make_tuple(x, y, z));
                                                                                                                                      	CO_END
                                                                                                                                      private:
                                                                                                                                      	int lineno = 0;
                                                                                                                                      	int x = 1, y = 1, z = 1;
                                                                                                                                      };
                                                                                                                                      
                                                                                                                                      PytripleGenerator pytriples()
                                                                                                                                      {
                                                                                                                                      	return PytripleGenerator();
                                                                                                                                      }
                                                                                                                                      
                                                                                                                                      int main()
                                                                                                                                      {
                                                                                                                                      	PytripleGenerator py;
                                                                                                                                      	for (int i = 0; i < 100; ++i)
                                                                                                                                      	{
                                                                                                                                      		auto [x, y, z] = py.next();
                                                                                                                                      		std::cout << "(" << x << ", " << y << ", " << z << ")\n";
                                                                                                                                      	}
                                                                                                                                      }
                                                                                                                                        +5

                                                                                                                                        Да, а потом читаешь чей-нибудь чужой код (особенно, если не в IDE, а в гитхабе, где GOTO нет), и пытаешься понять, что за магия тут происходит. Все-таки С++ это действительно скорее множество языков, а не один язык.

                                                                                                                                          0

                                                                                                                                          Вы не могли бы поянсить, как это колдунство работает?

                                                                                                                                            0

                                                                                                                                            Я так понимаю это практическое применение структурной теоремы :)

                                                                                                                                              +2
                                                                                                                                              Это было бы практическим применением той теоремы если бы кто-то и правда преобразовал код в комбинацию простых циклов и ветвлений.

                                                                                                                                              А там просто эксплуатируется способность оператора switch иметь метки внутри вложенных блоков кода.
                                                                                                                                                –1
                                                                                                                                                ( Есть какие-то недостатки в Boost?
                                                                                                                                                Boost.Coroutine — created by Oliver Kowalke, is the official released portable coroutine library of boost since version 1.53. The library relies on Boost.Context and supports ARM, MIPS, PowerPC, SPARC and X86 on POSIX, Mac OS X and Windows.
                                                                                                                                                Boost.Coroutine2 — also created by Oliver Kowalke, is a modernized portable coroutine library since boost version 1.59. It takes advantage of C++11 features, but removes the support for symmetric coroutines. )

                                                                                                                                                Это демонстрация отставания на десятки лет даже от Modula
                                                                                                                                                process оператор; ...  
                                                                                                                                                begin
                                                                                                                                                   прогсосрас; оператор  
                                                                                                                                                end прогупрзадач;
                                                                                                                                                

                                                                                                                                                То, что Модула небольшой и эффективный язык, не вызывает сомнения. Описанный Холденом и Вандом (1980) компилятор требует всего лишь 16К слов памяти PDP 11 и компилирует п строк программы примерно за (5 + n)/12 секунд на PDP 11/40. Кроме того, язык очень удобен на практике. Он использовался при реализации ряда достаточно сложных систем с большим успехом (см., например, Эндрюс, 1979, Рунсиман, 1980).

                                                                                                                                                не говоря о Modula-2
                                                                                                                                                в Модуле 2 от процессов отказались вовсе в пользу взаимодействующих подпрограмм (Вирт, 1980)


                                                                                                                                                Для демонстрации фичи оптимальность алгоритма обычно не имеет значения.


                                                                                                                                                Хорошо, отложим в сторону алгоритмы...

                                                                                                                                                В оригинале есть хотя бы частичное соответствие MISRA C:

                                                                                                                                                http://ericniebler.com/2014/04/27/range-comprehensions/
                                                                                                                                                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)
                                                                                                                                                            {
                                                                                                                                                                result += (x + y + z);
                                                                                                                                                                ++found;
                                                                                                                                                                if(found == 3000)
                                                                                                                                                                    goto done;
                                                                                                                                                            }
                                                                                                                                                        }
                                                                                                                                                    }
                                                                                                                                                }
                                                                                                                                                done:   
                                                                                                                                                


                                                                                                                                                Bartosz Milewski уже предпочитает демонстрировать «Getting Lazy with C»

                                                                                                                                                (
                                                                                                                                                речь о:

                                                                                                                                                
                                                                                                                                                --- Bartosz_Milewski_Bad_Style.c	Fri Feb 01 16:19:10 2019
                                                                                                                                                +++ ericniebler.com_2014_04_27_range-comprehensions.c	Fri Feb 01 16:19:00 2019
                                                                                                                                                @@ -1,11 +1,17 @@
                                                                                                                                                 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)
                                                                                                                                                             {
                                                                                                                                                                 result += (x + y + z);
                                                                                                                                                                 ++found;
                                                                                                                                                                 if(found == 3000)
                                                                                                                                                                     goto done;
                                                                                                                                                             }
                                                                                                                                                +        }
                                                                                                                                                +    }
                                                                                                                                                +}
                                                                                                                                                 done:   
                                                                                                                                                

                                                                                                                                                )
                                                                                                                                                0
                                                                                                                                                Всякий раз перед возвратом (CO_YIELD) из «корутины» мы запоминаем место (номер строки), откуда осуществляем возврат. Когда функция вызывается вновь, мы возвращаемся в это место (сразу после оператора return).
                                                                                                                                                Достигается это путём обёртывания всего тела функции в switch и расстановке меток case __LINE__: во всех местах возврата, благо switch в C/C++ позволяет прыгать даже внутрь вложенных блоков (самое известное применение чему — Устройство Даффа).