Учить С++ можно по-разному. Тезис, что учить C перед C++ вовсе не обязательно, верный. Эти языки эволюционировали отдельно, хотя и влияли друг на друга. Но как можно игнорировать факт, что C++ основан на C и вбирает в себя значительную его часть? Попытаемся проанализировать проблему с точки зрения целей такого обучения и методов их достижения.
Посмотрим выступление Кейт Грегори на конференции, почитаем учебный план «Яндекса» и статью Бьерна Страуструпа, создателя языка C++.
Часовое выступление Кейт Грегори на CppCon’е в 2015 «Перестаньте преподавать C» наделало шума. На одном из слайдов (он скрыт, его нет на видео, но слайды можно скачать и посмотреть) представлено резюме доклада:
«На сегодняшний день большинство людей, которые собираются помочь другим изучать C++, начинают с „введения в C“. И я верю, что это вносит большой вклад в то, что в мире появляется много плохого кода на C++. В последние годы я преподаю C++ (и даю рекомендации тем, кто учат его самостоятельно) совершенно по-другому. Никаких строк типа char*, strlen, strcmp, strcpy, printf и обычных массивов []. Указатели вводятся очень поздно. Ссылки идут до указателей, полиморфизм показывается на ссылках, а не указателях. Умные указатели, подобно и обычным „сырым указателям“ (как результат вызова new или операции &), оставлены до того времени, когда они понадобятся. Стандартная библиотека как можно раньше, и современный код на C++ с первого урока».
Как видно, отношение к указателям скептическое, они считаются «чрезмерно сложными» (challenging). В современном коде «сырые указатели» и правда нужны редко, но позже мы попробуем показать, к чему приведёт то, о чём говорит спикер. В большинстве случаев она, кстати, предлагает использовать ссылки.
Со ссылками синтаксис не меняется
Не нужно писать ->
Не нужно писать *
Не нужно указывать & при передаче аргумента
Всё это, безусловно, делает курс более enjoyable, как говорит Кейт.
Если забыть про безопасность и производительность, функции семейства printf() и правда могут показаться сложными.
cout << foo << endl;
// vs.
printf("%d\n", foo);
Нужно заучивать спецификаторы (%d, %i, %u, %c, %s...), но манипуляторы, которые используются для вывода данных в cout, ничуть не проще. Как альтернативу Кейт предлагает вообще не записывать ничего в вывод, а использовать в первое время отладчик. Или GUI (форму с полями ввода). Кстати, автор собирается рассказывать про отличия endl от '\n' или однажды это станет маленьким приятным сюрпризом для студентов?
Современный C++ настолько простой и элегантный, что даже её 12-летний сын Кевин с ним справлялся. Он написал программу, которая спрашивает имя и, если оно совпадает с именем в программе, приветствует его. На слайдах был следующий код.
string name;
// ...
string greeting = "Hello, " + name;
if (name == "Kate")
greeting += ", I know you!";
Автору такой программы не нужно ничего знать про нулевой символ '\0' как признак конца строки, функции для сравнения строк, функции конкатенации и их аргументы.
С вектором вместо массива всё столь же просто. Ровно до того момента, пока разработчик не сохранит ссылку на один из элементов или не начнёт удивляться, что производительность иногда проседает.
Все аналогии хромают, но, мне кажется, Кейт говорит примерно следующее: «Студентам-медикам незачем изучать курсы анатомии, физиологии и тем более химии или биологии. Это нужно учить как можно позже. Мы готовим врачей, а не учёных».
К видео написали много восхищённых комментариев, среди которых совершенно потерялась критика.
«Это сумасшествие. Если так учить C++ (особенно избегая указателей и массивов в стиле C), получатся программисты, которые не понимают, как работает их код. Такие структуры из STL, как vector или list внутри содержат массивы и указатели. Без этой базы нельзя будет понять (или труднее будет понять), например, почему у списков нет доступа к элементам по индексу или почему push_back у вектора иногда очень быстрый, а иногда нет (когда происходит реаллокация)».
Кто-то просто советует Rust, если нужен язык без такого наследия. А кто-то выступает совсем резко и предлагает сократить «Kate Gregory. Stop Teaching C» до «Kate Gregory. Stop Teaching».
Не очень понятна позиция Кейт об унаследованных от C возможностях, которые не связаны с указателями. К примеру, изучающий заинтересуется графикой и решит воспользоваться OpenGL. Насколько его удивит побитовый оператор |?
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
В C++ очень много ситуаций, которые вызывают неопределённое поведение (undefined behavior). В том числе это касается и побитовых операторов (|, &, ~, <<, >>), их нельзя использовать с отрицательными числами. Человеку, незнакомому с этим, выражение (-1) << 8 не покажется подозрительным (это пример из реального проекта).
Кейт предлагает использовать ссылки, а не указатели для полиморфизма. Есть известный пример — «проблема ромбовидного наследования». Множественное наследование, разрешённое в C++, грозит определёнными проблемами, в ответ на это изобрели виртуальное наследование. И вот такой простой пример приводит к указателям.
Сам механизм полиморфизма обычно построен на таблицах виртуальных функций. И это легко объяснить, если человек уже поработал с указателями на функции.
Аргумент main() — char* argv[] (массив указателей на char), наверное, вызовет истерику.
int main(int argc, char* argv[])
А что это то же самое, что и char** (указатель на указатель на char) — вообще колдовство?
int main(int argc, char** argv)
В университетах традиционно после базового курса C++ изучают алгоритмы и структуры данных, часто внутри построенные на указателях.
Кейт говорит о вещах, которые делают C++ тем, чем он и является. Одна из таких вещей — перегрузка операторов. Если в обращении к полю класса можно писать foo.bar вместо foo->bar (и избежать так ненавидимой стрелки), то в перегруженном операторе иногда нужно возвращать разыменованный указатель *this.
Бьерн Страуструп в одной из своих книг справедливо говорит, что «любой программист, практикующий объектно-ориентированное программирование и использующий безопасные по отношению к типам контейнеры (такие как контейнеры стандартной библиотеки), так или иначе приходит к контейнерам указателей». Заметим — «сырых» или «умных». И ни о каких «контейнерах ссылок» речи идти не может.
«В большинстве реализаций C++ код шаблонов функций реплицируется. Это хорошо с точки зрения производительности, но при недостаточной осторожности может привести к разбуханию кода [...] По счастью, имеется очевидное решение. Все контейнеры указателей могут разделить единственную специальную реализацию, называемую специализацией (specialization) [...] Для того чтобы определить специализацию, которую можно использовать для любого вектора указателей, требуется частичная специализация»:
template<class T>
class Vector<T*> : private Vector<void*>
А если придётся, к примеру, вместо сортировки целых теперь сортировать строки именно в стиле C? То же касается сравнения ключей в хранилищах типа key-value. Как прийти к идее специализации шаблона для char*, если весь путь от таких строк держались подальше?
template<>
bool less<const char*>(const char* a, const char* b)
{
return strcmp(a, b) < 0;
}
Ведь нужно не только писать новый код, ещё нужно поддерживать старый.
Итератор — объект, обобщающий понятие указателя. У него есть перегруженный оператор *, который означает примерно то же, что и указателя (разыменование), то есть обращение к тому объекту, на который «указывает» итератор. Для давно знакомого с указателями человека такая концепция не вызовет никаких затруднений.
Вполне посильное для новичка занятие — сделать небольшую игру, к примеру, с помощью библиотеки SFML. Человек узнает о рендеринге в отдельном потоке и испытает страх от столкновения с незнакомыми значками?
void renderingThread(sf::Window* window)
sf::Thread thread(&renderingThread, &window);
Кстати, на один вопрос Кейт из зала прозвучал такой ответ: «Я не пытаюсь вырастить разработчиков библиотек, я пытаюсь вырастить их пользователей». Больше того, спикер уверена, что так получаются «лучшие программисты» и те, «кому понравился курс».
И тут нужно задуматься: чем мы занимаемся? Инженерией или профанацией? Решаем задачи с помощью сложного, но эффективного инструмента или играем с высокоуровневыми возможностями языка, не задумываясь о потенциальных негативных последствиях?
«Яндекс» переделал свой курс по C++ уровня «белый пояс», когда там посмотрели выступление Кейт Грегори и пошли у неё на поводу.
Неделя 1
Обзор возможностей С++: Hello, world, обзор типов, операции с простыми типами, операции с контейнерами, языковые конструкции
Компиляция, запуск, отладка: установка Eclipse, создание проекта в Eclipse, отладка в Eclipse
Операции: присваивание, арифметические, логические
Условные операторы и циклы
Неделя 2
Функции: синтаксис, передача параметров по значению, ссылки как способ изменить переданный объект, const-ссылки как способ сэкономить на копировании, const защищает от случайного изменения переменной
Контейнеры: std::vector, std::map, std::set, взгляд в будущее: обход словаря с помощью structured bindings
Неделя 3
Алгоритмы и лямбды: min, max, sort; count, count_if, лямбды; современный аналог std::transform — for (auto& x: container)
Видимость и инициализация переменных
ООП: введение в структуры и классы
Неделя 4
ООП: примеры
Работа с текстовыми файлами и потоками
Перегрузка операторов
Встраивание пользовательских типов в контейнеры
Исключения
Неделя 5
Курсовой проект
Ответом на такой план стала шутка комментатора:
Курс высшей математики за четыре недели
Неделя 1: таблица умножения, элементарные арифметические операции, возведение в степень и логарифмы, тригонометрические функции
Неделя 2: производная функции, интегрирование
Неделя 3: вычисление определённых интегралов с помощью вычетов, формула Гаусса — Бонне
Неделя 4: применение дифференциальной геометрии в общей теории относительности
Неделя 5: доклад на международной математической конференции
В статье «Пять популярных мифов о C++» Бьерна Страуструпа первый же «миф» такой: «Чтобы понять С++, сначала нужно выучить С».
Так почему множество преподавателей проповедуют подход «Сначала С»? Потому, что:
они так всегда делали,
того требует учебная программа,
они сами так учились,
раз С меньше С++, значит, он должен быть проще,
студентам всё равно рано или поздно придётся выучить С
Какие аргументы он приводит в пользу нового подхода? Сначала нам показывают пример, где имя пользователя и домен, например, "gre" и "research.att.com", объединяются в электронный адрес "gre@research.att.com". В C++ это выглядит элегантно.
string compose(const string& name, const string& domain)
{
return name + '@' + domain;
}
В C-версии нужно выделить память (а пользователь функции должен не забыть её сам освободить), рассчитать нужный объём памяти (не забыть про нулевой символ), вызвать функции для копирования, не запутаться в их аргументах. Бьерн Страуструп подчёркивает, что первая версия более эффективная: не подсчитываются символы в аргументах, для коротких строк не используется динамическая память.
char* compose(const char* name, const char* domain)
{
char* res = malloc(strlen(name)+strlen(domain)+2); // место для строк, '@', и 0
char* p = strcpy(res,name);
p += strlen(name);
*p = '@';
strcpy(p+1,domain);
return res;
}
Конечно, предпочтительнее именно первый вариант, и правда то, что «благодаря С++11, С++ стал более дружественным для новичков». Но разве так не получатся программисты, которые не знают, в каких областях памяти что хранится и сколько конкретно места занимает?
Да что мелочиться, современные стандарты позволяют очень просто делать по-настоящему удивительные вещи. Примеры из той же статьи.
vector<int> v = {1,2,3,5,8,13};
for (int x : v) test(x);
for (int x : {1,2,3,5,8,13}) test(x);
Парадоксальный вывод — нужно скрыть от глаз новичков и, может, даже продолжающих все эти ужасные вещи, ведь и без них как-то получается. Но только «получается» до того момента, пока примеры не уходят дальше итерирования по массиву.
Есть базовый дидактический принцип: от простого к сложному. Мне видится, что происходит подмена одного другим. Что-то внешне простое на самом деле же является сложным, я постарался привести некоторые примеры.
Часть, совместимую с C, трудно игнорировать полностью. Сам C-стиль, конечно, может найти себе место в коде мало какого проекта. Нужно писать на современном C++. Но если начинать сразу с «современного C++», есть риск очень поверхностного знания, что в случае языка, где на каждом углу поджидает неопределённое поведение, может быть опасно. А если нужен Python, то возьмите его. Каждому инструменту своя задача.