Сейчас проходит конференция 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).
Вопросы? Комментарии?
Нет, я рассказал далеко не все про обучение. Лишь едва царапнул поверхность.
Вопросы из зала
[У Страуструпа есть сверхспособность отвечать по 5 минут на простые вопросы, поэтому я очень сильно сократил его ответы да и сами вопросы тоже]
Core Guidelines слишком всеобъемлющие. как учить?
Не нужно читать всё. Прочитать введение, затем раздел с философией. Не нужно искать правило, правило само найдет вас.
Нужны ли стандартной библиотеке нужны простые функции? Например random [я полагаю, что имеется в виду функция без необходимости установки начального значения и возможностью задания закона распределения]?
Да, нужны.
Вы говорили про 3 дистрибутива C++. Кто должен этим заниматься?
Вряд этим будет заниматься комитет, поэтому, я думаю, это нужно делать силами сообщества. Это будет проще с развитием единого пакетного менеджера и модулей
Моя дочь учится в колледже и мы вместе делали проект термостата. Так вот, для того, чтобы получить температуру и отобразить на экране, потребовался целый семестр изучения C++. Что вы думаете по этому поводу?
Да, есть такая проблема. С модулями будет лучше.
Нужно ли преподавать программирование как общий предмет, так же как математику
Я не компетентен в этом вопросе.
mmatrosov: вы говорили о том, что в обучении нужно пользоваться библиотеками. Не будет ли такого, что новое поколение программистов не будет знать основ?
Зависит от цели. Я учу студентов как реализовать вектор, они должны знать об указателях, но не каждому нужно реализовывать lock-free код.
Комментарии (24)
MindCucumber
01.10.2017 13:01+1Кстати, как дела обстоят со стандартным пакетным менеджером? Есть ли какие-то новости?
OnoIt
Жалко, что не спросили как правильно объяснять студентам Undefined Behaviour в С++.
A-Stahl
Я не преподаватель и не студент, но не вижу вообще никакой проблемы: Undefined Behaviour == error везде и всегда.
OnoIt
И если это была бы всегда ошибка на этапе компиляции, то писать на C++ было бы значительно легче.
RPG18
И сейчас можно чекать UB через UndefinedBehaviorSanitizer
OnoIt
Это замечательно. Есть ли такое для всех компиляторов С++?
RPG18
Достаточно писать unit тесты, автотесты и собирать с GCC/CLang
vsb
Это невозможно в общем случае. А там, где компилятор может определить UB, обычно он и сейчас выдаёт предупреждение, которое можно превратить в ошибку при желании.
khim
А если бы компилятор умел читать мысли и делал бы не то, что вы написали, а что вы задумали — было б вообще великолепно, да?
В том-то и дело, что почти любая строка в C++ потенциально может стать источником UB — а равно и какой-нибудь другой проблемой.
И, как верно заметили, у студентов UB не вызывает никаких особенных проблем.
Проблемы с UB — это всегда «горе от ума»: написано, что разименовывать nullptr нельзя… но я-то знаю, что произойдёт GPF!
Нет, не знаете. Сегодня произойдёт, завтра — нет. Написано UB — значит так делать нельзя. Точка. Конец истории.
Прекратите считать, что вы умнее разработчиков стандартов и компиляторов и вдруг, внезапно UB перестанет быть проблемой и станет помощником.
OnoIt
Вы пишите, что «почти любая строка в C++ потенциально может стать источником UB» — сомневаюсь, что это правда. Если же это так, то это ошибка дизайна языка.
Проблема С++ это то, что компилятор считает, что программист очень умный и его программа без UB и поэтому ее можно агрессивно оптимизировать. Поэтому перед изучением С++ стоит реально оценить, достаточно ли ты умный для этого языка и в случае сомнений выбрать другой язык.
khim
Нет, это специфика предметной области. Просто то, что, скажем, в Java приводит к выбросу какого-нибудь
ArrayIndexOutOfBoundsException
в C/C++ может привести к более серьёзным последствиям. Но также как почти любая строка в Java может выкинуть какой-нибудьNullPointerException
,ArrayIndexOutOfBoundsException
или ещё чего похуже, так любая строчка в C/C++ при «неподходящих» аргументах может привести к UB.Нет. Компилятор в Java так не считает, но бинарный поиск всё равно частенько не работает. «Тупой» и «предсказуемый» компилятор — вовсе не гарантия того, что ваша программа будет работать без ошибок…
Проблема C/C++ в том, что «тупые» компиляторы стали «умными», а некоторые разработчики, считающие себя умнее компилятора (и бывшие, в прошлом, реально «умнее» какого-нибудь Turbo C), «нарываются на неприятности» — после чего поднимают «вселенский вой» на форумах.
Если же UB избегать и на пытаться «домыслить» за компилятор, то программировать на C/C++ не сильно сложнее, чем на Java или C#…
Для студента это как раз несложно: сказали обязательно выделять память перд использованием (обращение к неинициализированному указателю — первый UB, на который «нарываются» новички) — выделяем, сказали не допускать переполнения целых чисел — не допускаем. Правил много, но ничего особенно страшного в них нет. А вот для программиста с опытом, который «точно знает», что переполнение ничем, кроме получения отрицательного числа там, где оно должно быть положительным, «не грозит» — поведение компилятора может действительно оказаться неожиданным…
OnoIt
Все таки UB — специфика дизайна языка. В универсальном ассемблере нельзя сказать, как будет обрабатываться переполнение на конкретном процессоре. А при программировании на ассемблере для конкретного процессора — можно.
Про Java пока не будем, а давайте обсудим пример отсюда:
blog.llvm.org/2011/05/what-every-c-programmer-should-know_14.html
Код:
становиться после оптимизации компилятора
Мне кажется, это немного печальная оптимизация.
И такой быстрый код совсем не нужен.
khim
А это — уже неважно. С и C++ — это инструменты для написания переносимого кода. Если вы пытаетесь при программировании на них использовать ваше знание конкретного процессора — вас ждут сюрпризы.
Давайте.
Типичный код написанный мистером компилятор-писали-дураки-я знаю-как-лучше. Кто заставлял вместо простого и понятного:
писать вот то, что там написали? Желание выпендриться? И вообще: почему там
int
, а неsize_t
? Чтобы было веселее?Если даже предположить, что у нас «тупой», не оптимизирующий компилятор, то вряд ли первоначальный вариант будет быстрее (вам нужно будет куда-то скопировать «size», потом увеличить его на единицу, только после этого сравнить… вместо того, чтобы использовать одну инструкцию
cmp
).Код изначально было ужасен, так стоит ли удивляться, что после вмешательства компилятора он перестал работать? Ровно то, о чём я говорил:
А теперь давайте вернёмся всё-таки к 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» нужно весьма немало знать!
RPG18
Это не C++, это Си.
dev26
Только для знаковых типов.
khim
Переполняться, в соответствии со стандартом, могут только числа со знаком: 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.
dev26
Это зависит от того, как определить переполнение. Т.е. с точки зрения здорового человека, для uint8_t: 250+10=4 — переполнение, но в C оно четко определено и нормально (хоть и называется по другому).
khim
К сожалению в природе нет здоровых людей, есть только недообследованные. Потому лучше по возможности использовать определения из стандарта — иначе можно потратить на споры о терминологии тысячи человеко-часов и до сути так и не добраться.
DistortNeo
Учитывая, что в большинстве своём программисты используют именно знаковые типы, утверждение
> Почти любая арифметика может привести к переполнению
является верным.
DistortNeo
В соседней теме объяснили, почему это невозможно. Неопределённое поведение — крайне полезная штука, позволяющая писать быстрый код. Например, то же целочисленное переполнение или доступ за границы массива — это тоже примеры неопределённого поведения. Да даже reinterpret_cast<> — чистый UB. Хотите отказаться от них?
А если хотите язык без UB, то не стоит смотреть в сторону C++.
enree
Не понимаю, почему все так выделяют UB. Такая же ошибка, как и многие другие, которые не ловятся компилятором. Например, открываем сокет и не освобождаем. Через пару дней работы программа перестает правильно работать, потому что сокеты кончились. И это не будет ошибкой на этапе компиляции. Поэтому правильный ответ — «Не пишите так. Никогда. Даже если вам кажется, что работает. И то, что ваши тесты сейчас проходят, не значит ничего.». Плюс многие вещи могут отловиться статическими анализаторами, так что не игнорируйте их предупреждения без веской причины.
OnoIt
Не так просто — посмотрите habrahabr.ru/post/216189.
А утечку ресурсов можно обнаружить при тестировании.
DistortNeo
А ещё про различие Undefined и Unspecified behavior.