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


Это первое вступительное видео. Оно не такое интересное для меня, но пропустить тоже не мог, это же Страуструп. Далее, текст от его лица. Заголовки взяты из слайдов.



Disclaimer: весь дальнейший текст — достаточно краткий пересказ, являющийся результатом работы моего восприятия, и то, что я посчитал "водой" и проигнорировал, могло оказаться важным для вас. Иногда выступление было таким: "(важная мысль 1)(минута воды)(важная мысль 2)". Эти две мысли плавно перетекали друг в друга, а у меня получались довольно резкие скачки. Где можно сгладил, но посчитал нецелесообразным полностью причесывать текст, на это бы потребовалось много времени.


Вступление


Когда меня попросили выступить на открытии конференции, я задумался, о чем же я могу рассказать такого, что важно для вас, и чего вы не слышали миллион раз. И я решил рассказать про обучение языку C++.


Мы все учителя и мы все ученики


Зададимся вопросом кого мы учим, чему, зачем и как. Нужно делать это лучше. Я не критикую кого-то в частности, но чувствую, что мы должны делать это лучше. Не все из нас преподаватели, но тем не менее постоянно возникают случаи, когда мы занимаемся обучением. Например, рассказываем коллегам о последних фичах или даем советы. Общаемся на StackOverflow, Reddit, ведем блоги и т.д. Но нужно давать хорошие советы. Советы, которые двигают мир вперед.


Есть одна вещь, которая сильно беспокоит меня — зачастую у людей бывают очень странные представления о том, что собой представляет C++. Чуть позже я вернусь к этой проблеме.


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


Обучение программированию


Не нужно фокусироваться на языковых фичах. Например, вы встречали примеры в которых объясняется проблема приведения signed short к unsigned int [рассказывается о преподавании языка в общем, а не об особенностях C++]. Это неинтересно и можно увидеть в отладчике или прочитать в руководстве. Учите так, чтобы такая проблема не появлялась.


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


Одна из встечающихся проблем обучения C++ — то что язык изучается сам по себе, отдельно от библиотек. Вектор на 697 странице, sort через 100 страниц. Это учит, что stl скучная, сложная фигня. И в то же время: свой Linked List или Hash table это круто, круче чем stl.


Не будьте слишком умными


[в выступлении автор использует слово clever с негативным оттенком, что-то вроде человека, который пытается казаться быть умным]


Люди которые хотят и требуют "самое последнее" часто не знают основ. Пересмотрите основы.


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


Фокусируйтесь на общих случаях. Будьте рациональными. Не говорите ученикам "Делай только так, это правильно, это закаляет характер. И можете получить пятерку, если ответите именно так". Нужно объяснить, почему нужно следовать правилам, дать ученикам хорошие идеалы, идеи, техники.


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


Обучение программированию


Если изучать только сам язык, то попав в реальность можно просто "утонуть".
Используйте различные инструменты. Не только компилятор и учебник, но и IDE, отладчики, системы контроля версий, профилировщики, модульное тестирование, статические анализаторы, онлайн компиляторы. Инструменты должны быть современными (иногда получаю вопросы по Turbo C++ 4.0 :( )


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


Язык это не только синтаксис


Как мы часто учим? Объясняем язык плюс немного стандартную библиотеку. Без всякой графики, пользовательского интерфейса, веба, электронной почты, баз данных… И многие ученики считают, что C++ скучный бесполезный язык. Но это же не так, ведь такие вещи как браузеры, СУБД, САПР и прочие пишутся на C++. Перед началом лекции потратьте 5 минут о практическом применении.


Мы должны сделать лучше


Нам, сообществу C++, очень важно упростить начало работы, возможность пользоваться "прямо сейчас".


Как программирование похоже на занятие фотографией?


Как пользователи в различных отраслях разделяются на группы? Приведем пример с фотографией. Результат зависит от оборудования и от пользователя. Лично я новичок в фотографии. Большинство возможностей профессиональной фотокамеры будут для меня бесполезными. Она много весит, дорого стоит. Для нее существует множество аксессуаров в которых можно утонуть. Но с ее помощью можно делать превосходные фотографии, если потратить много времени на обучение. Аналогично существует много людей, которые не могут использовать разнообразные фичи языка и библиотеки.


Массовый рынок


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


Как-то во время преподавания мне было нужно, чтобы у студентов была установлена библиотека GUI. Оказалось, что установить одну и ту же библиотеку на студенческие Mac, Linux, Windows, весьма болезненно.


Языку нужна "система"


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


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


Какими должны быть основные дистрибутивы?


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


Модули помогут


База:


import bundle.entry_level; //Для новичков
import bundle.enthusiast_level; //Для продвинутых
import bundle.professional_level; //Для профессионалов

Расширения (которые не входят в базу):


import grahics.2d;
import grahics.3d;
import professional.graphics.3d;
import physlib.linear_algebra;
import boost.XML;
import 3rd_party.image_filtering;

Нужны хорошие пакетные менеджеры и системы сборки


Как ученик на вторую неделю после начала обучения может установить библиотеку графического интерфейса и работы с базами данных? Различные библиотеки и системы собираются по разному. Различные библиотеки могут быть плохо совместимыми. Десяток несовместимых пакетных менеджеров — это не решение. Нужно сделать простым выполнение простых задач


> download gui_xyz
> install gui_xyz

Или эквивалентным способом, например в IDE:


import gui_xyz; //в коде

Современный C++


Мое видение современного C++ (как обычно):


  • Статическая типобезопасность, хорошо определенные интерфейсы.
  • Безопасность ресурсов (конструкторы/деструкторы, RAII).
  • Абстракции без накладных расходов.
  • Инкапсуляция, инварианты.
  • Обобщенное программирование, шаблоны.
  • Простота для большинства разработчиков, сложность скрыта в библиотеках.

Меняться трудно


Современный C++ это не C, Java, C++98 и не тот язык, на которым вы программировали 10 лет назад. Инерция — враг хорошего кода. Преподаватели, оправдывая неиспользование современных стандартов, говорят, что "мы так не делаем", "это не вставить в мою учебную программу", "может быть через 5 лет". У студентов появляется большее доверие к интернету, чем к преподавателям. Некоторые считают, что они умнее преподавателей, и иногда они правы. У меня стабильно каждый год на курсе были студенты, абсолютно убежденные, что они умнее меня в программировании. В этих частных случаях, я обоснованно уверен, что оне не правы [смех в зале].


Что такое современный С++?


  • Лучшие практики, использующие текущий стандарт ISO С++
  • Стремление к типо- и ресурснобезопасному коду

Для реализации этого 2 года назад был открыт проект C++ Core Guidelines. Он дает конкретные ответы на вопросы. У него много много участников, включая Microsoft и Red Hat.


Примеры кода


Не отделяйте примеры от объяснения. 5 страниц голой теории это лишняя трата. Давайте примеры и объяснения к ним. Без объяснения люди не обобщают. Они просто копипастят и сами изобретают трактовку, причем иногда очень странную.


Улучшение кода


Всегда объясняйте причины. Например:


//1
int max = v.size();
for(int i = 0; i < max; ++i)

//2
for (auto x : v)

Почему 2 лучше чем 1? Пример 2 явно показывает намерение, v может быть изменен без переписывания цикла, и менее подвержен ошибкам. Следует заметить, что 1 предоставляет более гибкие возможности. Но ведь goto еще более универсален, и поэтому мы избегаем его.


  • I.4: Делайте интерфейсы точными и строготипизированными

[I.4 означает пункт из Core Guidelines]


void blink_led1(int time_to_blink) //Плохо - неясный тип

void blink_led2(milliseconds time_to_blink) //Хорошо

void use()
{
  blink_led2(1500); //Ошибка: какая единица измерения?
  blink_led2(1500ms);
  blink_led2(1500s); //Ошибка: неверная единица измерения
}

[Здесь milliseconds какой-то простой тип не из библиотеки Chrono, поэтому последняя строчка приводит к ошибке. Ниже по тексту описано обобщение типа для единицы измерения, взятого из Chrono. Если интересно, можете почитать мое описание этой библиотеки]


template<class ep, class period>
void blink_led(duration<rep, period> time_to_blink)
{
  auto ms_to_blink = duration_cast<milliseconds>(time_to_blink);
}

void use()
{
  blink_led(2s);
  blink_led(1500ms);
}

  • ES.20: Всегда инициализируйте объект
  • F.21: Для возврата нескольких значений из функций предпочитайте использовать tuple, структуру (или structured binding).

Error_code err; //неинициализировано: потенциальная проблема
//...
Channel ch = Channel::open(s, &err); //out-параметр: потенциальная проблема
if(err) { ... }

Лучше:
auto [ch, err] = Channel::open(s) //structured binding
if(err) ...

А должен ли этот код использовать возврат двух параметров?


  • E.1: Прорабатывайте стратегию отлова ошибок в начале разработки
  • E.2: Бросайте исключение для уведомления того, что функция не может выполнить задачу
  • E.3: Используйте исключения только для уведомления об ошибках

auto ch = Channel::open(s);

Лучше? Да, если неуспешное открытие было предусмотрено в программе.


Улучшение кода: не будьте слишком умными


Слово "умный" в контексте использования C++ — ругательное. Найдите баг:


istream& init_io()
{
    if(argc > 1)
        return *new istream { argv[1], "r" };
    else
        return cin;
}

  • P.8: Не допускайте утечки
  • R.11: Избегайте прямого вызова new и delete
  • R.4: Raw-ссылка (T&) должны быть невладеющей

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


  • P.1: Выражайте идеи прямо в коде
  • NL.1: Не говорите в комментариях, что и так ясно видно в коде
  • NL.2: Выражайте намерения в комментариях
  • NL.3: Поддерживайте комментарии в актуальном состоянии

//Плохо
auto x = m * v1 + vv //Перемножение m с v1 и прибавление vv

//Хорошо
void stable_sort(Sortable& c)
//сортирует "c" согласно порядку, задаваемым "<"
//сохраняет исходный порядок равных элементов (определяемыми "==")
{
    //...несколько строк нетривиального кода
}

Philosophy rules


Я рекомендую вам отправиться на github и почитать раздел Philosophy rules, содержащий основные концепции.


Core guidelines


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


Сейчас разрабатываются 2 открытых проекта: анализатор, для проверки правил Core Guidelines, и библиотека GSL — guidelines support library (реализация от Microsoft).


No Garbage Collector!


Вопросы? Комментарии?


Нет, я рассказал далеко не все про обучение. Лишь едва царапнул поверхность.


Вопросы из зала


[У Страуструпа есть сверхспособность отвечать по 5 минут на простые вопросы, поэтому я очень сильно сократил его ответы да и сами вопросы тоже]


Core Guidelines слишком всеобъемлющие. как учить?

Не нужно читать всё. Прочитать введение, затем раздел с философией. Не нужно искать правило, правило само найдет вас.


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

Да, нужны.


Вы говорили про 3 дистрибутива C++. Кто должен этим заниматься?

Вряд этим будет заниматься комитет, поэтому, я думаю, это нужно делать силами сообщества. Это будет проще с развитием единого пакетного менеджера и модулей


Моя дочь учится в колледже и мы вместе делали проект термостата. Так вот, для того, чтобы получить температуру и отобразить на экране, потребовался целый семестр изучения C++. Что вы думаете по этому поводу?

Да, есть такая проблема. С модулями будет лучше.


Нужно ли преподавать программирование как общий предмет, так же как математику

Я не компетентен в этом вопросе.


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

Зависит от цели. Я учу студентов как реализовать вектор, они должны знать об указателях, но не каждому нужно реализовывать lock-free код.

Комментарии (24)


  1. OnoIt
    01.10.2017 02:07
    +1

    Жалко, что не спросили как правильно объяснять студентам Undefined Behaviour в С++.


    1. A-Stahl
      01.10.2017 11:52
      +1

      Я не преподаватель и не студент, но не вижу вообще никакой проблемы: Undefined Behaviour == error везде и всегда.


      1. OnoIt
        01.10.2017 13:10
        -2

        И если это была бы всегда ошибка на этапе компиляции, то писать на C++ было бы значительно легче.


        1. RPG18
          01.10.2017 16:32
          +1

          И сейчас можно чекать UB через UndefinedBehaviorSanitizer


          1. OnoIt
            01.10.2017 20:25

            Это замечательно. Есть ли такое для всех компиляторов С++?


            1. RPG18
              01.10.2017 23:01

              Достаточно писать unit тесты, автотесты и собирать с GCC/CLang


        1. vsb
          01.10.2017 18:03
          +4

          Это невозможно в общем случае. А там, где компилятор может определить UB, обычно он и сейчас выдаёт предупреждение, которое можно превратить в ошибку при желании.


        1. khim
          01.10.2017 19:01

          А если бы компилятор умел читать мысли и делал бы не то, что вы написали, а что вы задумали — было б вообще великолепно, да?

          В том-то и дело, что почти любая строка в C++ потенциально может стать источником UB — а равно и какой-нибудь другой проблемой.

          И, как верно заметили, у студентов UB не вызывает никаких особенных проблем.

          Проблемы с UB — это всегда «горе от ума»: написано, что разименовывать nullptr нельзя… но я-то знаю, что произойдёт GPF!

          Нет, не знаете. Сегодня произойдёт, завтра — нет. Написано UB — значит так делать нельзя. Точка. Конец истории.

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


          1. OnoIt
            01.10.2017 20:45
            -1

            Вы пишите, что «почти любая строка в C++ потенциально может стать источником UB» — сомневаюсь, что это правда. Если же это так, то это ошибка дизайна языка.

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


            1. khim
              01.10.2017 22:45

              Вы пишите, что «почти любая строка в C++ потенциально может стать источником UB» — сомневаюсь, что это правда.
              Почти любая арифметика может привести к переполнению. Переполнение — это UB.

              Если же это так, то это ошибка дизайна языка.
              Нет, это специфика предметной области. Просто то, что, скажем, в Java приводит к выбросу какого-нибудь ArrayIndexOutOfBoundsException в C/C++ может привести к более серьёзным последствиям. Но также как почти любая строка в Java может выкинуть какой-нибудь NullPointerException, ArrayIndexOutOfBoundsException или ещё чего похуже, так любая строчка в C/C++ при «неподходящих» аргументах может привести к UB.

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

              Проблема C/C++ в том, что «тупые» компиляторы стали «умными», а некоторые разработчики, считающие себя умнее компилятора (и бывшие, в прошлом, реально «умнее» какого-нибудь Turbo C), «нарываются на неприятности» — после чего поднимают «вселенский вой» на форумах.

              Если же UB избегать и на пытаться «домыслить» за компилятор, то программировать на C/C++ не сильно сложнее, чем на Java или C#…

              Для студента это как раз несложно: сказали обязательно выделять память перд использованием (обращение к неинициализированному указателю — первый UB, на который «нарываются» новички) — выделяем, сказали не допускать переполнения целых чисел — не допускаем. Правил много, но ничего особенно страшного в них нет. А вот для программиста с опытом, который «точно знает», что переполнение ничем, кроме получения отрицательного числа там, где оно должно быть положительным, «не грозит» — поведение компилятора может действительно оказаться неожиданным…


              1. OnoIt
                02.10.2017 01:13
                -2

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

                Про Java пока не будем, а давайте обсудим пример отсюда:
                blog.llvm.org/2011/05/what-every-c-programmer-should-know_14.html
                Код:

                void process_something(int size) {
                  // Catch integer overflow.
                  if (size > size+1)
                    abort();
                  ...
                  // Error checking from this code elided.
                  char *string = malloc(size+1);
                  read(fd, string, size);
                  string[size] = 0;
                  do_something(string);
                  free(string);
                }
                

                становиться после оптимизации компилятора
                void process_something(int *data, int size) {
                  char *string = malloc(size+1);
                  read(fd, string, size);
                  string[size] = 0;
                  do_something(string);
                  free(string);
                }

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


                1. khim
                  02.10.2017 04:33

                  В универсальном ассемблере нельзя сказать, как будет обрабатываться переполнение на конкретном процессоре.
                  И именно поэтому переполнение — это UB.

                  А при программировании на ассемблере для конкретного процессора — можно.
                  А это — уже неважно. С и C++ — это инструменты для написания переносимого кода. Если вы пытаетесь при программировании на них использовать ваше знание конкретного процессора — вас ждут сюрпризы.

                  а давайте обсудим пример отсюда:
                  blog.llvm.org/2011/05/what-every-c-programmer-should-know_14.html
                  Давайте.

                  // Catch integer overflow.
                  if (size > size+1)
                    abort();
                  
                  Типичный код написанный мистером компилятор-писали-дураки-я знаю-как-лучше. Кто заставлял вместо простого и понятного:
                  // Catch integer overflow.
                  if (size = INT_MAX)
                    abort();
                  
                  писать вот то, что там написали? Желание выпендриться? И вообще: почему там int, а не size_t? Чтобы было веселее?

                  Если даже предположить, что у нас «тупой», не оптимизирующий компилятор, то вряд ли первоначальный вариант будет быстрее (вам нужно будет куда-то скопировать «size», потом увеличить его на единицу, только после этого сравнить… вместо того, чтобы использовать одну инструкцию cmp).

                  Код изначально было ужасен, так стоит ли удивляться, что после вмешательства компилятора он перестал работать? Ровно то, о чём я говорил:
                  Некоторые разработчики, считающие себя умнее компилятора (и бывшие, в прошлом, реально «умнее» какого-нибудь Turbo C), «нарываются на неприятности» — после чего поднимают «вселенский вой» на форумах.


                  А теперь давайте вернёмся всё-таки к Java. Заметьте, что код, о котором там говорится вызывает на C/C++ как раз UB — но ведь и в Java он тоже не работает, пусть и по другому!

                  И вот это — типичная ситуация. UB делает программирование на C/C++ сложнее только тогда, когда вы начинаете аппелировать вот к этим вот при программировании на ассемблере для конкретного процессора — можно. C/C++ — не ассемблер — это, по большому счёту, всё, что нужно знать про то, чтобы при его использовании не возникало проблем с UB.

                  Ещё раз: где-то 99% всех бед, которые я наблюдал от «оптимизаций, базирующихся на UB» происходят из-за того, что кто-то пытается быть «слишком умным». Так вот: «шибко умным» нужно было быть с каким-нибудь Borland C++ 2.0 или каким-нибудь Zortech C++ 3.1. В те времена реально можно было отиграть заметный процент перейдя от индексов к указателям в цикле, например. В XXI веке это не нужно, а в последнее время — становится вредным.

                  Всё, что вам нужно помнить об UB это одну фразу: «не выпендривайтесь» — и этого достаточно в 99% случаев. Я про это уже писал.

                  Именно поэтому вопрос UB — это достаточно болезненная тема в C/C++ сообществе, но… не при обучении программированию! Студенты, в большинстве своём, просто недостаточно продвинуты, чтобы UB их «укусил»: чтобы написать код, который на неоптимизирующем компиляторе будет работать, а на оптимизирующем «упрётся в UB» нужно весьма немало знать!


                1. RPG18
                  02.10.2017 11:39

                  Это не C++, это Си.


              1. dev26
                04.10.2017 13:04

                Переполнение — это UB.


                Только для знаковых типов.


                1. khim
                  04.10.2017 14:35

                  Переполняться, в соответствии со стандартом, могут только числа со знаком: A computation involving unsigned operands can never over?ow, because a result that cannot be represented by the resulting unsigned integer type is reduced modulo the number that is one greater than the largest value that can be represented by the resulting type.


                  1. dev26
                    04.10.2017 14:49

                    Это зависит от того, как определить переполнение. Т.е. с точки зрения здорового человека, для uint8_t: 250+10=4 — переполнение, но в C оно четко определено и нормально (хоть и называется по другому).


                    1. khim
                      04.10.2017 16:21

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


                1. DistortNeo
                  04.10.2017 14:39

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

                  > Почти любая арифметика может привести к переполнению

                  является верным.


      1. DistortNeo
        01.10.2017 14:54
        +3

        В соседней теме объяснили, почему это невозможно. Неопределённое поведение — крайне полезная штука, позволяющая писать быстрый код. Например, то же целочисленное переполнение или доступ за границы массива — это тоже примеры неопределённого поведения. Да даже reinterpret_cast<> — чистый UB. Хотите отказаться от них?

        А если хотите язык без UB, то не стоит смотреть в сторону C++.


    1. enree
      01.10.2017 14:47
      +1

      Не понимаю, почему все так выделяют UB. Такая же ошибка, как и многие другие, которые не ловятся компилятором. Например, открываем сокет и не освобождаем. Через пару дней работы программа перестает правильно работать, потому что сокеты кончились. И это не будет ошибкой на этапе компиляции. Поэтому правильный ответ — «Не пишите так. Никогда. Даже если вам кажется, что работает. И то, что ваши тесты сейчас проходят, не значит ничего.». Плюс многие вещи могут отловиться статическими анализаторами, так что не игнорируйте их предупреждения без веской причины.


      1. OnoIt
        01.10.2017 14:56

        Не так просто — посмотрите habrahabr.ru/post/216189.
        А утечку ресурсов можно обнаружить при тестировании.


    1. DistortNeo
      01.10.2017 14:57
      +2

      А ещё про различие Undefined и Unspecified behavior.


  1. MindCucumber
    01.10.2017 13:01
    +1

    Кстати, как дела обстоят со стандартным пакетным менеджером? Есть ли какие-то новости?