Короткий звіт про тиждень що я його провів на зльоті представників комітету зі стандартизації С++, розробників компіляторів та бібліотек, розробників що займаються оптимізацією коду та “промислових” представників галузі: розробників ігор, дослідників ведучих наукових закладів та представники найвідоміших компаній.
Усього було присутньо більше 600 людей. Щодня сесії на різні теми починалися 0 9 ранку, закінчувалися о 6 вечора з 2-годинною перервою на обід. О 8 вечора починалися воркшопи (класи) де можна було поговорити скажімо з авторами бібліотек boost, представниками комітету зі стандартизації С++ або іншими видатними людьми. Паралельно доповіді тривалістю в 1-1.5 години читалися у шести аудиторіях проіменаваних на честь видатних вчених: Паскаль, Ньютон, Ферма, Лейбніц, Декарт та Ейлер.
Цього року більшість доповідей було присвячено паралельному програмуванню (~80%) на відміну від попередніх років коли акцент робився на метапрограмування (шаблони).
Багато також говорили про новий стандарт С++14 в який увійде все те що було недороблено в С++11. А вже в С++17 буде багато нових змін які будуть підкориговані в С++20.
Ну і декілька речей які особисто мені запам’яталися.
Всюди auto
Сучасна рекомендація полягає в тому щоб використовувати auto де тільки можна. І суть в тому що це не лише коротший запис, але і безпечніший: скажімо при зміні типів в алгоритмах/інтерфейсах/структурах данних на відбувається прихованого приведення типів. Тому замість
for (list:const_iterator it = l.begin(); it != l.end(); ++it) {...use(*it);...}
В С++11 можна (і треба) писати
for (const auto& i: l) {...use(i);...}
А в С++14 стане можливим навіть
for (i: l) {...use(i);...}
Але авто-виведення корисне не лише для циклів та ітераторів, але і для лямбд, які за новим стандартом можуть повертати лямбди як результат:
auto plus = [](auto x){ retur [](auto y){ return x + y; } };
auto plus4 = plus(4);
auto s6 = plus4(2);
auto s10 = plus(4)(6);
Правила виведення типів
Через додання авто-виведення типів в мову довелося додати правила за якими це виведення відбувається. Усього цих правил більше 10 зараз і тим хто розробляє бібліотеки треба їх усі знати і розуміти дуже добре. І в цілому тенденція така що мова для тих хто пише біблітетеки мова стає складнішою і дозволяє робити тонкіші речі, а для тих хто користується бібліотеками мова навпаки стає простішою.
Так от для виведення типів треба враховавати що діють правила як для шаблонів, а для шаблонів тип шаблону залежить від параметрів методів. Крім того авто-типи втрачають константніть/волатильність. Ну і для типів-значень правила виведення завдяки слайсінгу складніші ніж для вказівників та посилань.
Нові рекомендації для програмістів:
- для обов’язкових параметрів які не будуть змінюватися використовувати const &
- для обов’язкових параметрів які будуть змінюватися використовувати &
- для необов’язкових параметрів використовувати *
- для оптимізаційних трюків і передачі прав володіння використовувати && та std::move(). Але при цьому важливо на 100% розуміти що робиш бо такий код майже гарантовано вбиває всю оптимізацію яку міг би зробити компілятор.
Ніяких new/delete
Залиште new/delete людям які пишуть біблітотеки та аллокатори, почніть використовувати unique_ptr/shared_ptr. І не забувайте що unique_ptr є фактично безкоштовним і фактично це голий вказівник, але безпечний! А shared_ptr використовуйте коли у об’єкта може бути лише один власник.
Наприклад замість
widget* f();
void n(){
widget* w = f();
gadget* g = new gadget();
use(*w, *g);
delete g;
delete w;
}
Треба писати
unique_ptr<widget> f();
void n(){
auto w = f();
auto g = make_unique>gadget>();
use(*w, *g);
}
Небезпечний код
Це була довга доповідь з трьох частин що зайняла майже весь день і була присвячена тому як писати небезпечний код.
У короткому підсумку код
do_something();
cleanup();
Не є небезпечним через те що в ньому ніяк не перехоплюються та не оброблюються виключення і не даються гарантії що алоковані ресурси буде звільнено у правильному порядку. Із застосуванням методіки RAII (Resource Acquisition Is Initialization) небезпечний код має виглядати так:
do_something();
Досягається це дотриманням простих правил:
- конструктор ніколи не кидає виключення та не викликає код який може кинути виключення само собою
- так само і деструктор ніколи не кидає виключення
- оператор присвоєння ніколи не кидає виключення і має бути транзакційним. Досягається це створенням тимчасового об’єкту який і ініціалізують як копію, а потім просто замінюють, щось типу такого:
class A {
A& operator=(const A& a)
{
A tmp(a);
using std::swap;
swap(tmp);
return *this;
}
До речі звернічть увагу що swap рекомендується використовувати саме так і не викликати std::swap() на випадок якщо swap() перевантажено для типу.
Ну і взагалі там неймовірно багато усього було. Наприклад розбір того чому код
f(shared_ptr(new A()), shared_ptr(new B());
}
є небезпечним. Ось тут https://github.com/CppCon/CppCon2014/blob/master/Presentations/Exception-Safe%20Code/Exception-Safe%20Code%20-%20Jon%20Kalb%20-%20CppCon%202014.pdf?raw=true файл з усіма слайдами з цієї доповіді.
С++ на Марсі
Виступав ведучий програміст проекту Куріосіті (http://mars.jpl.nasa.gov/msl/) і розповідав в основному те як працює система руху цієї машини. Той С++ що вони там використовують це дуже порізана мова: нема шаблонів, виключень, потоків, множинного успадкування, перевантаження операторів, тощо.
У них там кожна підсистема незалежна і продубльована. Наприклад система руху на останньому марсоході побудувана на платі з процесором PowerPC RAD750 (133 MHz), має 128 Мб пам’яті, використовує ОС VxWorks. В цій ОС є пакетна обробка задач і розділена пам’ять, тому вони написали свій менеджер пам’яті.
Взагалі цікаво що 4 з 19 камер марсоходу належать системі руху (по 2 на кожен комп’ютер) і використовуються майже як людина використовує очі. Максимальна швидкість складає 1.5 см на секунду, але після цього треба зупинитися, зробити стерео-фото двома камерами, розбити катринку на сітку врахувавши перспективу і оцінити кожну клітинку після чого вибрати оптимальний/найнебезпечніший маршрут. Прикол ще в тому що колеса можуть або крутитися, або повертати, одночасно і те і інше не можливо робити. За день марсохід може зробити максимум 100 метрів.
Прикольні баги там у них. Наприклад коли машинці довірили самостійно проїхати кілька десятків метрів вона зупинилася і відмовилася їхати далі тільки коли прокручування (сковзання) колес перевищило допустимий показник. З’ясувалося що заїхали в пісчану дюну і наступні 2 місяці витратили щоб вручну вивести машину з ловушки (так, кожну команду відсилали на Марс окремо). Тобто за 2 місяці щоденної роботи проїхали назад 20 метрів.
Ну і був у них там якийсь баг з пам’яттю через який комп’ютер почав перевантажуватися невпинно. На той момент коли баг знайшли і пофіксали Марс сховався за Сонцем і довелося чекати кілька місяців доки з’явиться пряма видимість
С++ в іграх
Представники галузі говорили в основному про оптимізацію коду і на кожен слайд з С++ кодом у них було по 3-5 слайдів з асемблером. Всякі хитрі трюки оптимізації що вимагають знання принципів роботи заліза і таке інше.
Але ось вам кілька цікавих фактів: в коді Assassin’s Creed 6.5 мільйонів (!) рядків коду на С++ самої гри плюс 9 мільйонів рядків С++ інструментів (це різні редактори, пакувальники і таке інше) та 5 мільйонів рядків на С#. При цьому в коді гри нема RTTI, виключень, шаблонів (ні STL, ні boost).
Кілька нових фіч
Це все або ось-ось з’явиться, або вже підтримується деякими компіляторами (читайте у вікіпедії подробиці):
- constexpr дозволяють писати код який виконується в процесі компіляції.
- бар’єри пам’яті (дуже складна тема сама по собі) неймовірно спрощують написання програм у паралельному стилі і є набагато дешевшими за синхронізацію: lock_guard, conditional_variable, atomic, compare_exchange_weak/compare_exchange_strong. Про це все треба читати, воно не те щоб складне, але треба якісь приклади писати для пояснення.
- atomic<T> можна буде використовувати для shared_ptr. Звучить це як щось звичайне, але насправді така підтримка дозволить уникнути багатьох проблем у реалізації lock-free алгоритмів та значно скоротити і спростити код.
- обмеження шаблонів – можна буде вказати що шаблон чи функція призначені, наприклад, лише для колекцій які можна сортувати:
template<typename S, typename T>
requires Sequence<S> &&
Sortable<S> &&
Equality_compare<Value_type<S>, T>
Iterator<S> sort(S& seq);
...
sort(vector{ 5, 4, 1 }); //Ok
sort(list{5, 4, 1 }); // Error: not Sortable
Ну або скоротити все це до
auto sort(Sortable& seq);
Можна буде писати свої обмеження якось ось так:
requires(C x){ { f(x)} –> int }
що означає “має бути можливим викликати функцію f для об’єкту типу С і потім привести результат до типу int”.
Зараз комітет працює над визначенням списку стандартних обмежень.
Звісно при всьому бажанні я не можу переказати всього що там було, ось вам кілька посилань щоб почитати самостійно: