5 популярних міфів про C++

частина 1

оригінальна стаття


Далі розповідь буде вестись від першої особи

0. Вступ

Ця стаття складається з 3 частин, де я обговорю та розвію 5 популярних міфів про C++:

  1. “Щоб зрозуміти C++, спочатку потрібно вивчити C”
  2. “C++ — об’єктно-орієнтована мова”
  3. “Для надійного програмного забезпечення необхідна збірка сміття (Garbage Collection)”
  4. “Для більшої ефективності необхідно писати низькорівневий код”
  5. “C++ призначена лише для великих складних програм”

Якщо Ви вірите хоча б в один із цих міфів або маєте колег, які їх дотримуються, то ця коротка стаття для Вас. Деякі з цих міфів були правдою для певних людей, певного завдання і деякий час. Так чи інакше, з сьогоднішнім C++, використовуючи широкодоступні компілятори та інструменти ISO C++ 2011, вони всього лише міфи.

Я вважаю ці міфи популярними, оскільки часто їх чую. Іноді вони підтримуються через певні причини, але частіше їх сприймають як дещо очевидне, не потребуюче доказів. Іноді їх використовують для того, щоб відмовити від використання C++.

Для того, щоб розвіяти кожен з цих міфів, знадобилася б довга стаття або навіть книга, але я маю на меті просто обговорити це та коротко висловити свої аргументи.

1. Міф 1: “Щоб зрозуміти C++, спочатку потрібно вивчити C”

Ні. Вивчати базове програмування, використовуючи C++, значно простіше, ніж використовуючи C.

C є майже підмножиною C++, але ця підмножина не є кращою для першого знайомства з мовою, бо мові C не дістає підтримки позначень, безпеки типів та легшої у використанні стандартної бібліотеки, наданої в C++ для спрощення елементарних задач. Розглянемо звичайну функцію для компонування e-mail адреси:

Її можна використовувати наступним чином:

Версія цього коду в C вимагає явного маніпулювання символами та явного керування пам’яттю:

Її можна використовувати так:

Яку версію ви б викладали? Яка версія простіша у використанні? Чи вірно я зрозумів, що версія на C? Ви впевнені? Чому?

На кінець, яка версія, скоріш за все, буде ефективнішою? Так, версія на C++, тому що там немає потреби рахувати кількість символів аргументів і вона не використовує вільний простір (динамічну пам’ять) для коротких рядків аргументів.

1.1 Вивчення C++

Це не єдиний приклад, я вважаю цю ситуацію типовою. Тож чому так багато викладачів наполягають на тому, що “спочатку треба вивчити C”?

— Тому що так вони робили роками.
— Тому що це те, чого вимагає навчальна програма.
— Тому що так вчились викладачі у молодості.
— Тому що C вважається простішою для використання за C++ через свій розмір.
— Тому що рано чи пізно студентам все одно доведеться вивчати C (або підмножину C з C++).

Так чи інакше, C — далеко не найпростіша і не найзручніша підмножина C++ для початку. Більше того, якщо ви знаєте достатню частину C++, підмножину C вивчити буде просто. Вивчення C перед C++ призводить до жахливих помилок, яких легко можна уникнути в C++, та вивчення методик для їх уникнення.

Для ознайомлення з сучасним підходом до викладання C++, використовуйте книгу Programming: Principles and Practice Using C++. В кінці книги навіть є глава, в якій показується, як використовувати C. Цю книгу використовували, очевидно вдало, десятки тисяч студентів-початківців з декількох університетів. Друге її видання використовує можливості C++ 11 та C++ 14 для полегшення навчання.

З C++ 11, C++ стала більш доступною для новачків. Наприклад, ось вектор зі стандартної бібліотеки, ініціалізований із послідовністю елементів:

У C++ 98 ми могли ініціалізувати лише масиви зі списками. У C++ 11 ми маємо змогу визначити конструктор, що прийматиме список ініціалізації { } для будь-якого типу за нашим бажанням.

Ми могли б пройти цей вектор діапазонним циклом for:

Це викличе метод test() по одному разу для кожного елемента v.

Діапазонний цикл for може пройти будь-якою послідовністю, тож ми могли спростити цей приклад, використавши напряму список ініціалізації:

Однією з цілей C++ 11 було зробити прості речі дійсно простими. Звісно, це було реалізовано без додаткових втрат продуктивності.

2. Міф 2: “C++ — об’єктно-орієнтована мова”

Ні. C++ підтримує ООП та інші стилі програмування, але не можна навмисно обмежувати її, називаючи лише “об’єктно-орієнтованою”. Вона підтримує синтез технологій програмування, включаючи об’єктно-орієнтоване та узагальнене програмування. Часто найкраще рішення задачі включає в себе більше одного стилю (“парадігми”). Під найкращим я розумію найкоротше, найбільш зрозуміле, найбільш ефективне, найлегше у супроводі і т.д.

Міф про те, що C++ є об’єктно-орієнтованою мовою, змушує людей надавати перевагу C при виборі між C++ та C, коли насправді їм не потрібні великі ієрархії класів з безліччю віртуальних (run-time поліморфних) функцій — для багатьох людей та багатьох задач це дуже невдалий вибір. Віра цьому міфу призводить також до того, що інші засуджують C++ за не чисту об’єктно-орієнтованість. Зрештою, якщо вважати за “хороше” об’єктно-орієнтоване, то C++, очевидно, містить багато не об’єктно-орієнтованого, і, відповідно, має бути визнана “не дуже хорошою”. В будь-якому випадку, цей міф дає гарний привід не вивчати C++.

Розглянемо приклад:

Чи є цей код об’єктно-орієнтованим? Звісно є; він критично опирається на ієрархію класів з віртуальними функціями. Чи є він узагальненим? Звісно є; він критично спирається на параметризований контейнер (vector) і узагальнену функцію for_each. Чи є він функціональним? В деякій мірі; він використовує лямбда-вираз (конструкція [ ]). То що ж це? Це сучасна C++: C++ 11.

Я використав і діапазонний цикл for, і алгоритм foreach зі стандартної бібліотеки для того, щоб показати особливості. У справжньому коді я б використав лише один цикл, який міг би написати будь-яким із цих способів.

2.1. Узагальнене програмування

Чи хотілося б Вам трохи узагальнити цей код? Врешті-решт, зараз він працює лише із векторами вказівників на фігури (Shapes). Як щодо списків та вбудованих масивів? Що стосовно “розумних вказівників” (вазівників для управління ресурсами), таких як shared_ptr та unique_ptr? Що стосовно об’єктів, які не називають фігурами (Shapes), але їх можна намалювати (draw() ) та повернути (rotate() )? Розглянемо:

Цей код спрацює для будь-якої послідовності, якою можна пройти від першого (first) до останнього (last) елемента. Це стиль алгоритмів C++ зі стандартних бібліотек. Я використав auto, щоб уникнути необхідності називати типом для інтерфейсу щось на зразок “об’єкти, що схожі на фігури”. Ця функція C++ 11 означає “використовувати тим виразу, який використовується для ініціалізатора”, тож для for-циклу тип p буде тим самим, що і тип first. Використання auto для позначення типу аргументу для лямбда-виразу є функцією C++ 14, але вже використовується.

Розглянемо:

Тут я роблю припущення про те, що Blob є певним графічним типом, що має операції draw() та rotate(), а також що Container є певним типом контейнера. Список стандартної бібліотеки (std::list) має функції begin() та end(), щоб допомогти користувачеві пройти послідовність його елементів. Це миле та класичне ООП. Але що, якщо Container є чимось, що не підтримує поняття проходження напіввідкритої послідовності [b:e) зі стандартної бібліотеки? Чимось, що не має членів begin() та end()? Я ніколи не бачив нічого контейнер-подібного, що не можна було обійти, тому ми можемо визначити вільні у положенні begin() та end() з відповідною семантикою. Стандартна бібліотека передбачає таку можливість для C-стилізованих масивів, тому якщо Container є масивом стилю C, проблема вирішена — і масиви у стилі C досі є дуже поширеними.

2.2. Адаптація

Розглянемо складніший випадок: що, якщо Container зберігає вказівники на об’єкти та має іншу модель доступу та обходу? Наприклад, припустимо, що ви маєте отримати доступ до такого контейнера:

Цей стиль не є поширеним. Ми можемо перетворити його на послідовність [b:e) наступним чином:

Враховуйте, що ця модифікація є неагресивною: я не мав потреби вносити зміни в Container або певну його ієрархію, щоб перетворити Container до моделі обходу, що підтримується стандартною бібліотекою C++. Це форма адаптації, а не форма рефакторингу.

Я обрав цей приклад, щоб показати, що ці узагальнені методології програмування не обмежуються стандартною бібліотекою (у якій вони набули широкого розповсюдження). Також, за найбільш поширеним визначенням “об’єктно-орієнтованого”, вони не є об’єктно-орієнтованими.

Ідея про те, що код на C++ має бути об’єктно-орієнтованим (що означає повсюдне використання ієрархій та віртуальних функцій), може серйозно зашкодити продуктивності. Так чи інакше, ООП є відносно жорстким (не кожен пов’язаний тип вписується у ієрархію), і виклик віртуальних функції погіршує вбудовування (а це може коштувати вам 50-кратного фактору у швидкості в простих та важливих випадках).

Післямова

У моїй наступній статті я обговорюватиму твердження “Для надійного програмного забезпечення необхідна збірка сміття (Garbage Collection)”