• Достаточно ли будет чтения cppreference для освоения STL?

    vt4a2h
    @vt4a2h Куратор тега C++
    Senior software engineer (C++/Qt/boost)
    Не слишком понятно, что имеется ввиду под "освоением STL". Это всего лишь библиотека языка. Вам надо знать, грубо говоря, что в ней есть в плане возможностей. А уж потом, по мере необходимости, идти и читать документацию.

    Проще говоря, допустим случай, когда вам надо отсортировать массив. Зная, что в STL есть и класс для массива и методы сортировки, вы идёте и читаете документацию, а потом используете. cppreference -- отличный ресурс с примерами.

    Я помню читал одну относительно тоненькую книгу по STL когда-то. Вроде бы неплохая была. Сейчас уже наверно устарела: "STL tutorial and reference guide" by David R. Musser .
    Ответ написан
    Комментировать
  • Как правильно нумеровать версии программы?

    vabka
    @vabka
    Токсичный шарпист
    Есть ли какие-то определенные правила?

    Правила есть разные. Из популярного - уже упомянутый https://semver.org/lang/ru

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

    Самая простая система нумерации - просто с каждым новым релизом увеличивать номер версии на 1.
    Ещё вариант, который по сути даже и не является номером - брать в качестве версии хэш коммита из git.

    как правильно

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

    firedragon
    @firedragon
    Не джун-мидл-сеньор, а трус-балбес-бывалый.
    странный у вас вопрос вот что пишут про ржавчину

    В Rust объектная система основана на типажах (traits) и структурах (structs). Типажи определяют сигнатуры методов, которые должны быть реализованы для каждого типа (чаще всего — структуры), реализующего типаж. Типаж может содержать и реализации методов, принимаемые по умолчанию. Реализация типажей для данной структуры, а также реализация собственных методов структуры обозначается ключевым словом impl. Язык содержит несколько десятков встроенных типажей, большая часть которых используется для перегрузки операторов, а некоторые имеют специальное значение.

    Rust поддерживает аналогию наследования типажей — типаж может требовать от реализующего типа реализацию других типажей. Однако языковой поддержки наследования самих типов, и следовательно, классического ООП, в Rust нет. Вместо наследования типов, аналогия иерархии классов реализуется введением типажей, включением структуры-предка в структуру-потомка или введением перечислений для обобщения разных структур[31].

    Язык поддерживает обобщённые типы (generics). Помимо функций, обобщёнными в Rust могут быть комплексные типы данных, структуры и перечисления. Компилятор Rust компилирует обобщённые функции весьма эффективно, применяя к ним мономорфизацию (генерация отдельной копии каждой обобщённой функции непосредственно в каждой точке её вызова). Таким образом, копия может быть адаптирована под конкретные типы аргументов, а следовательно, и оптимизирована для этих типов. В этом отношении обобщённые функции Rust сравнимы по производительности с шаблонами языка C++.
    Ответ написан
    Комментировать
  • Как исправить ошибку сегментации C++ (segmentation fault)?

    wataru
    @wataru Куратор тега C++
    Разработчик на С++, экс-олимпиадник.
    Segmentation fault - это ошибка работы с памятью. Ваша программа вылезает за границы вашей памяти или делаете что-то не то с указателями.

    Одна очевидная ошибка - вы не уменьшаете n при удалении элемента. И тогда на следующей итерации вы будете что-то делать с элементом за концом массива.

    Еще одна ошибка у вас в том, что вы в add, например, всегда удаляете указатель на f. А в самом начале этот указатель неинициализирован. Удаление такого случайного указателя - это undefined behavior. Программа может упасть сразу, а может только на следующей итерации цикла.

    Советую инициализировать f в nullptr и перед удалением всегда проверять, что удаяемый казатель не нулевой.
    Ответ написан
    2 комментария
  • Почему при вызове деструктора не меняется переменная?

    wataru
    @wataru Куратор тега C++
    Разработчик на С++, экс-олимпиадник.
    Я так понимаю, у вас проблема со строчкой
    aobj1[0] = a(2);

    Тут вызывается конструктор для временного значения a. Потом оператор копирования из временной переменной в *aobj. Потом вызывается деструктор временного значения.

    А потом где-то в конце произойдет и деструктор aobj.

    У aobj delete_counter после этой строчки равен 1 (ведь он скопирован у временного значения, которое сделало delete_counter единицей в констукторе). В конце при вызове деструктора aobj там delete_counter будет 1 в начале.

    Вы смотрите на адрес this в дебагере в деструкторе. Два вызваных деструктора будут для двух разных объектов (для временного значения и для aobj).

    Если вы хотите какой-то счетчик ссылок делать, то вам надо переопределять операторы копирования и перемещения (а так же все возможные конструкторы). И там аккуратно изменять счетчик ссылок. И счетчик ссылок должен быть частью общего объекта - частью класса b, а не класса a.
    Ответ написан
    4 комментария
  • Как решить проблему с исключением в моем коде?

    wataru
    @wataru Куратор тега C++
    Разработчик на С++, экс-олимпиадник.
    Ошибка в недрах стандартной библиотеки. Вы в вашей программе что-то не так делаете и она падает. Это частая проблема в C/C++, можно допустить такие ошибки, что программа упадет.

    Вы уже открыли вашу программу в дебаггере. В правом нижнем углу вы видите стек. Первые 3 функции - это в недрах библиотеки. Вам тут ничего не понятно, поднимайтесь вверх. Ткните в четвертую строчку - там где Main().

    Увидите, что ошибка происходит на строчке:
    cout << morze.find( text[i] )->second <<" ";

    Проблема возникает при попытке обратиться к second у возвращенного значения. Но почему? Читайте ошибку: "cannot dereference end map/set iterator".

    Т.е. find возвращает end() итератор. Действительно, посмотрите в документацию - map вернет end() если искомого ключа в нем нет.

    Иправить ошибку просто - исправьте ваш код. Сначала присвойте переменной возвращенный из find итератор, и потом проверяйте, а не end() ли он. И только в противном случае выводите.

    Я подозреваю, что символом оказался пробел. Пробела в вашем мапе morze нет, вот все и падает.
    Ответ написан
    1 комментарий
  • Для чего нужны замыкания в C++ и как вы их используете?

    @Mercury13
    Программист на «си с крестами» и не только
    Замыкание — это способ передать в callback, из какого контекста он запустился.
    struct Row {
      int data[10];
    };
    struct Table {
      Row rows[10];
    }

    Нам нужно отсортировать таблицу по j-му столбцу. Номер j заранее неизвестен.

    int sortJ;
    
    int myCompare(const void* a,const void* b) {
      int ia = reinterpret_cast<Row*>(a)->data[sortJ];
      int ib = reinterpret_cast<Row*>(b)->data[sortJ];
      if (ia < ib) return -1;
      if (ia == ib) return 0;
      return 1;
    }
    
    int someJ = 5;
    sortJ = someJ;
    qsort(table.rows, 10, sizeof(Row), myCompare);

    Вот эта переменная sortJ — по какому столбцу сортировать — это и есть замыкание. Но, как известно, «избегай незнакомых женщин и глобальных переменных». Поэтому на STL мы делаем функтор (объект-функцию) и эту информацию кидаем в него.

    class MyCompare {
    public:
      MyCompare(int aJ) : j(aJ) {}
      bool operator () (const Row& a, const Row& b) const
        { return (a.data[j] < b.data[j]); }
    private:
      const int j;
    }
    
    int someJ = 5;
    std::sort(table.rows, table.rows + 10, MyCompare(someJ));

    Вот мы и избавились от глобальной переменной, закинув наше замыкание в private-поля объекта.

    Что плохо? Не будем говорить про технические тонкости. С точки зрения красоты и лаконичности кода: код слишком разлапистый. И тут пришёл C++11.
    int someJ = 5;
    std::sort(table.rows, table.rows + 10,
      [someJ](const Row& a, const Row& b) -> bool { return (a.data[someJ] < b.data[someJ]); } );

    Корявовато, но таков синтаксис Си++. Автоматически создаётся объект-функтор, и someJ становится его полем. Вот оно, замыкание — [someJ] — то есть те вещи, которые надо протащить внутрь функтора.

    Из реального проекта. Отбегал поток автоматического поиска нового регистрационного ключа; если что-то получилось — синхронно вызываем лямбду через Qt’шный механизм «сигнал-слот». Чтобы всё было синхронно, нужен объект, живущий в главном потоке (и он в интерфейсе MainControl — управление главной формой — тоже есть). Но тогда придётся вызывать не слот, а лямбду. Этой лямбде нужны два поля: fReregKey (новый ключ защиты от копирования) и fMainControl. Оба они в this, его и замыкаем.
    connect(this, &RenewThread::needUpdate, fMainControl.maincQobj(),
                [this]() {
            drm::createFile(fReregKey);
            fMainControl.maincLayoutOnRegister();
        });


    А теперь посмотрим в WinApi. Первая попавшаяся функция из DirectInput.
    HRESULT EnumObjects(
             LPDIENUMDEVICEOBJECTSCALLBACK lpCallback,
             LPVOID pvRef,
             DWORD dwFlags
    );
    
    BOOL DIEnumDeviceObjectsCallback(
             LPCDIDEVICEOBJECTINSTANCE lpddoi,
             LPVOID pvRef
    );

    Про pvRef говорится: функции EnumObjects он ни на что не нужен; что функция приняла, то и даст в callback. Тоже форма замыкания: можно передать указатель на любые данные, которые нужны callback’у.
    Ответ написан
    4 комментария
  • Как составлять header файл для статической библиотеки?

    wataru
    @wataru Куратор тега C++
    Разработчик на С++, экс-олимпиадник.
    А зачем это делать? Чем вас 8 хедеров не устраивают? Если уж так хочется сэкономить строчки в коде пользователей библиотеки, то можно воспользоваться рекурсивностью препроцессора и сделать хедер с 8 инклудами. Копипастить код точно не надо.
    Ответ написан
    1 комментарий
  • Как исправить ошибку "Нарушение прав доступа при записи по адресу"?

    wataru
    @wataru Куратор тега C++
    Разработчик на С++, экс-олимпиадник.
    Эта ошибка означает, что вы пишите в какую-то память, которая вам не принадлежит.

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

    Можно во время отладки нажать на "продолжить" и тогда дебаггер остановится именно на той инструкции, которая вызвала ошибку. Дальше уже можно смотреть, в какую переменную вы там пишите и откуда она взялась.

    Падает оно потому, что нельзя string читать и писать в файл вот так, просто интенрпретируя память объекта как char*. Потому что string содержит в себе указатели на динамически выделенную память.

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

    Вы можете так писать в файл только структуры из простых типов, и то не всегда (там всякое выравнивание может сыграть с вами злую шутку). Надо писать собственные методы сериализации и десериализации. Строку можно сохранять, например, как длину и потом столько байт, сколько нужно. Тогда при считывании вы сначала чистаете сколько-то байт размера, а потом нужное количество байт в саму строку.
    Ответ написан
    1 комментарий
  • Как и на чем написать андроид приложение?

    Adamos
    @Adamos
    Qt
    Ответ написан
    Комментировать
  • Почему lambda не захватывает this?

    gbg
    @gbg Куратор тега C++
    Любые ответы на любые вопросы
    Банальная порча всех итераторов, ссылок и указателей в тот момент, когда вы вызываете emplace у вашей свалки потоков второй раз. Лечение - избавиться от переаллокации при вызове emplace, путем вызова reserve(), или использовать контейнер, который не передвигает свое содержимое туда-сюда, например, std::list
    Ответ написан
    Комментировать
  • Для каких примерно целей программисту нужен computer science?

    @Arlekcangp
    Разработчик, Лид, Архитектор ПО
    Для любой задачи которую без CS не решить. В предыдущих ответах часть задач уже перечислили. Но это всё касается специализированных задач. Никто не гарантирует вам, что в какой то момент у вас подобных задач не встретится на рядовом до сего времени проекте. Обычно это происходит когда проект вырастает за рамки какого-либо фреймворка, который до того покрывал все 100% таких задач. Банальный пример: было однопоточное приложение. Оно перестало справляться с нагрузкой. По совету с Хабр QA (ну или стэковерфлоу - не важно )) приняли решение переписать на параллельные вычисления. А не у кого нет даже базовых знаний какие существуют "грабли" (опять банальный пример - "состояние гонки" может и маститого профессора CS свести с ума, а, как в том анекдоте про каплю никотина, лошадь и хомяка, "голову вайтишника разрывает на куски") Поэтому пока у вас в команде есть кто-то со знаниями может пусть не всего CS, а хотя бы каких то базовых вещей, а вы просто кодите, то вам оно и не нужно. (как выше замечено - на "галерах" это не актуально. Хотя даже там вообще то такие люди обычно есть и получают они вдвое больше. Иначе какой им смысл там оставаться)
    Ответ написан
    Комментировать
  • Assignment operator VS Destructor + Placement new, где аргумент placement new - prvalue?

    @dolusMalus
    С++ programmer from gamedev
    Мне кажется, что данный вопрос стоит рассмотреть с точки зрения нескольких позиций:
    1. Безопасность относительно исключений. Какой уровень гарантий должен предоставлять данный метод?
    2. Производительность. Критично ли получить максимально эффективный код или на данном этапе этим можно пожертвовать?
    3. Оценить проектное решение в свете известных и проверенных практик, например, принципы SOLID


    Для начала определяемся используются ли вообще исключения в Вашем проекте и если нет, то можно переходить сразу к вопросу о производительности. В противном случае, рассмотрим каждый случай:
    int32_t index = sizeof(T) * head;
            (reinterpret_cast<T *>(&buffer[index]))->~T();
            new (&buffer[index]) T(std::forward<Args>(args)...);
            head = (head + 1) % maxSize;

    данный вариант не предоставляет даже базовых гарантий (basic exception guarantee), т.к. если конструктор сгенерирует исключение, то мы уже закончили время жизни объекта вызвав деструктор, но не изменятся size и head; а значит мы пришли в невалидное состояние.

    int32_t index = sizeof(T) * head;
            (reinterpret_cast<T *>(&buffer[index]))->~T();
            new (&buffer[index]) T(std::forward<Args>(args)...);
            head = (head + 1) % maxSize;

    Можем сделать развилку по noexcept для конструктора с помощью SFINAE или if constexpr и в случае, если конструктор является noexcept; то оставить данный код. В противном случае придется уже действовать в зависимости от необходимого уровня гарантий, однако решения будут достаточно громоздкие: в добавок к развилке надо будет ловить исключение, пробрасывать его дальше, при этом восстанавливать объект или уменьшать счетчики и т.п. Более того можно вообще не обеспечить сильную гарантию при определенных условиях. Как видите, это уже сильно осложнило решение.
    Теперь к другому варианту:
    // Assignment operator
    //        (*(reinterpret_cast<T *>(&buffer[sizeof(T) * head]))) = T(std::forward<Args>(args)...);

    Здесь ситуация относительно лучше, т.к. проблемы могут быть только на уровне оператора присваивания, что как минимум перекладывает часть ответственности в сторону автора типа T. Однако воспользовавшись решением от Евгений Шатунов можно относительно легко получить достаточно понятный и "чистый" код на уровне сильных гарантий. Также стоит посмотреть в сторону copy and swap идиомы, как близкой к данной проблеме.
    По итогу при необходимости строгих гарантий стоит отдать предпочтение варианту с временным объектом.

    С точки зрения производительности, если нет необходимости в максимальной оптимизации, то стоит отдать предпочтение более понятному коду. Данный момент уже прекрасно освещен в ответе Евгений Шатунов, поэтому не вижу смысла повторяться. Однако формально если отбросить предположения об оптимизациях, то вариант с реконструированием по месту (деструктор -> конструктор) оптимальней, т.к. гарантировано не требует выделения дополнительных ресурсов от временного объекта и только две операции + нет необходимости в относительно сложном анализе на перемешаемость/копируемость. В случае со swap, мы можем таки попасть на копирование в зависимости от перемещаемости типа T.

    И часто забываемый, но крайне важный пункт про проектирование. Здесь нарушен Single responsibility принцип, что возможно и породило этот вопрос. Т.е. у Вас метод по добавлению элемента может удалять/заменять элементы, что должно вызвать вопросы. Более того, вы решили за клиента вашего API (даже если это и Вы сами) как нужно обрабатывать исключительную ситуацию по переполнению. Потом например вы решите, что в одном месте стоит сложить старые элементы в отдельную очередь в другом залогировать или удалять не по одному, а сразу половину буффера. Все это потребует переписывания метода add, а зачем и почему? Попробуйте убрать эту часть кода заменив на выбрасывание исключения или вариант с возвращением успешности операции (менее грамотное решение, но это уже из области субъективной оценки) и посмотреть как увеличится прозрачность и простота написания клиентского кода для этого метода. Еще стоит посмотреть на сходное проектное решение в std::vector и его методе pop_back. Подумайте почему он не возвращает удаленный элемент?

    Итого, если важна производительность и не важна работа с исключениями; то разумно выбрать вариант с реконструированием; иначе обмен с временным объектом. Но не стоит забывать всегда про анализ проектного решения и правильную ли Вы проблему вообще решаете.
    Ответ написан
    2 комментария
  • В чем смысл определения const int &ref=1;?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Для начала стоит немного пробежаться по категориям выражений в современном C++.
    Из статьи будет видно что 1 имеет категорию prvalue. 1 является литералом и не является строковым литералом. Это - литерал с типом int по умолчанию.

    Если снова обратиться к статье и посмотреть свойства prvalue, то будет видно, в частности, что prvalue:
    • имеют всегда полный тип;
    • не имеют размещения, а следовательно не имеют адреса в памяти;
    • могут использоваться для инициализации cvq-lvalue-ref и rvalue-ref.

    Последний пункт говорит о том, что код const int& ref = 1; или int&& ref = 1; является полностью стандартным.
    ref в этом случае будет ссылаться на переданный литерал и отсутствие размещения литерала этому не помеха.

    Смысл подобных выражений в том чтобы позволить написать такой код.
    void foo( const int& ref );
    
    void bar()
    {
      foo( 1 );
    }


    Смыл конкретно кода const int &ref=1; можно найти в том, чтобы не писать магические константы в коде. ref - очень плохое имя. Но голая 1 в коде еще хуже.
    Инженер ПО свой код пишет не для транслятора, а для своих сотрудников. Транслятор разберется в любом коде и по любому синтаксически верному коду всегда произведет работающий исполняемый код. Но другие люди в коде смогут разобраться только тогда, когда этот код написан доступным для понимания образом.

    Чтобы код было легче понять, его документируют. Документация бывает разная. Это могут быть комментарии, это может быть отдельный документ. Но лучше всего код понимается тогда, когда он "самодоукментируется" [1], [2], т.е. когда сам код однозначно поясняет цели своего существования и принципы своей работы.
    Чтобы код сам себя пояснял, имена функций, переменных и констант, имена типов и прочих рукописных конструкций должны как можно более ясно отражать цели своего существования. В частности, чтобы код не пестрил безликими магическими числами [?], эти числа принято обличать в т.н. поясняющие константы.
    Такую константу можно определить как cvq-lvalue-ref, назначить понятное в контексте кода имя и присвоить ей требуемый литерал.
    Ответ написан
    Комментировать
  • Как написать простой калькулятор?

    @majstar_Zubr
    C++, C#, gamedev
    Я отвечу для случая работы с простым форматом ввода:
    12345 + 6789

    1) вы не модифицируете входную строку, поэтому, если имеем дело со стандартом от c++17, то лучше в функцию принимать явно string_view или const string_view&, если ниже, то принимать надо const string&.

    2) если уж принимаем что-то строкоподобное, то смело пользуемся функцией-членом ::find, которая есть и в строке и в вью. С её помощью можно найти сразу позицию арифметического символа. Для простого случая, даже не нужен обход от найденной позиции к началу строки и к концу строки для проверок std::isspace, является ли символ пробелом, поскольку мы можем сразу слать в atoi для левого числа например string_view::substr(0, opPos - 1). И, получится, только один if/switch, который будет сопоставлять символ операции, например, с указателем на функцию.
    ...
    int plus(int left, int right) { return left + right; }
    
    using CalculatorFunction = int (*)(int, int);
    ...
    CalculatorFunction operation;
    ...
    switch (opChar):
    case '+': operation = plus;
    ...


    3) настоятельно рекомендую после этого реализовать нормальный простой калькулятор, а потом третьим заходом добавить поддержку скобок, hex oct бинарную и с мантиссой нотации записи чисел, операцию степени и корня.
    Нормальный - имеется ввиду, что в калькулятор входит строка, которая является корректным арифметическим выражением. В этом случае нужно ещё иметь дело с разделителями. По-хорошему, решение состоит из нескольких этапов:

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

    - как вариант, токены могут быть структурами с членом string_view на строковые представления и членом на тип токена (число, операция).

    - на втором логическом этапе мы считываем последовательность токенов и делаем какое-то действие:
    • считали число - ожидаем за ним токен операции
    • если оказался не токен операции - бросаем исключение о некорректном арифметическом выражении, иначе - подготавливаем аргументы и кормим функцию evaluate<T>( Token::kOperation op, T arg1, T arg2) , для случая, если операции нужно два аргумента, то первый уже считан, а второй будет в последовательности токенов за операцией. Результат evalaute записываем в переменную результата, которая, кстати говоря, типа double.
    • продолжаем движение по последовательности токенов до конца, контейнером может быть std::list, когда добрались до конца, возвращаем результат.
    Ответ написан
    Комментировать
  • Почему не вызывается конструктор копирования?

    hePPer
    @hePPer
    похоже на пропуск конструктора при оптимизации
    Copy elision
    Ответ написан
    4 комментария
  • Как изменять что-любо в программных проектах?

    saboteur_kiev
    @saboteur_kiev Куратор тега Программирование
    software engineer
    Большинство книг, которые выпущены вчера, базируются на знаниях, которые кто-то получал в 2004, поэтому даже в старых книгах всегда полно полезной информации.

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

    Или уточните вопрос.
    Ответ написан
    Комментировать
  • Как вывести несколько MessageBox на C++?

    jcmvbkbc
    @jcmvbkbc
    "I'm here to consult you" © Dogbert
    MessageBox -- модальный. Невозможно одновременно вывести несколько MessageBox из одного потока. Можно создать несколько потоков и в каждом из них вызвать MessageBox.
    Ответ написан
    Комментировать
  • Насколько перспективна смена профессии на разработчика С++?

    tsarevfs
    @tsarevfs
    C++ developer
    Если у человека хорошая математика, физика то учить C++ достаточно перспективно. Сложный код про моделирование физических систем, робототехнику часто пишут на C++. Знание языка и программирования в таких задачах бывает вторично.
    Писать сайты, особенно фронт с таким бекграундом может быстро стать достаточно скучно.
    Можно посмотреть на Python с ML. Тоже очень близко по интересам, а спрос огромный.
    Ответ написан
    Комментировать
  • Как называются разработчики, которые пишут не качественный код, но делают продукты?

    gbg
    @gbg
    Любые ответы на любые вопросы
    Быдлокодер. Индусокодер. За примерами - govnokod.ru

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

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

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

    Хороший код до выхода на рынок должен быть переписан хотя бы дважды, пройдя experimental -> staging -> release, а не вот это вот все.

    Говнокод же получается от работы в режме expelsease (фигак-фигак - и в продакшн).
    Ответ написан
    2 комментария