Прим. перекладача: цей текст є перекладом уривку з виступу Герба Саттера, присвяченого використанню auto. Джерело варто прочитати повністю, але саме ця частина цікава тим, що тут дається відповідь на аргументи, наведені у попередній статті про auto
.
Противники використання auto
часто кажуть, що «код стає менш читабельним, оскільки не ясно, якого саме типу змінна. Доводиться витрачати час на пошук оголошення функції або розбиратись з тим, який кінцевий тип буде у виразу». Частково це правда, особливо у випадку виразів auto x = expr;
. Також правда, що будь-яку функціональність можна використовувати надмірно. Однак, в цілому цей аргумент не здається мені важливим, оскільки я маю проти нього два не дуже важливих заперечення, і ще два дуже серйозних.
Не такі важливі контраргументи виглядають так:
- При використанні
auto
все ще можна прямо задавати тип виразу. - Аргумент не має значення, якщо ви працюєте в IDE — вона підкаже вам тип змінної, лише поворухніть мишкою. Втім, він все ще існує, коли мова йде про код поза IDE, наприклад, якщо його видрукували.
Але важливішими є два інші контраргументи:
- Такий підхід зміщується до програмування реалізації, а не інтерфейсу (implementations, not interfaces). Надмірна любов до прямо визначених типів робить код менш загальним та більш залежним, а тому крихким та обмеженим.
- Насправді ми (тобто, ви) ігноруємо типи весь час...
Думаєте, що це не так? Розглянемо такий код:
template<class Container, class Value>
void append_unique( Container& c, const Value& v ) {
if( find(begin(c), end(c), v) == end(c) )
c.emplace_back(v);
assert( !c.empty() );
}
Швидке питання: як ви гадаєте, скільки типів згадується у цій функції? Спробуйте дати найбільшу можливу версію.
Подумайте...
... і можна назвати правильну відповідь: нуль, нічогісінько. Якщо прискіпуватись, то один тип все ж є: це void
. Але це ж те ж саме що «без результату», тобто його тут і немає фактично.
У цьому коді не згадується жоден конкретний тип; завдяки цьому він стає більш потужним і нічого не втрачає у зрозумілості. Ясно, що ви не завжди писатимете код для шаблонів — але суть прикладу у тому, що вам не потрібно знати тип, щоб зрозуміти код, тому auto
можна використовувати всюди.
Отже, спробуємо перелічити всі випадки, коли ми ігноруємо типи. Перший з них — параметри функції:
-
Якого типу
Container
? Ми не знаємо, і це чудово... підійде все, у чого є методиbegin
,end
,emplace_back
таempty
. Справді чудово, бо такий підхід відповідає принципу відкритості/закритості та дозволяє розширювати функціональність — навіть через багато років ми зможемо додати новий тип іappend_unique
буде з ним коректно працювати. Цікаво, що навіть пропозиція концепцій для шаблонів, яка зараз перебуває на розгляді у комітеті стандартизації, не змінить роботу нашої функції. Зверніть увагу, наскільки більш потужним є такий підхід у порівнянні з тим, що використовується у традиційних об'єктно-орієнтованих фреймворках. Там, де контейнери наслідуються від якогось базового класу чи інтерфейсу, з'являється зв'язаність; також там неможливо легко розширити функціональність до будь-якого іншого типу. Важливо, що незнання типу дозволяє нам не обмежуватись приналежністю типу до якоїсь ієрархії. Цю неймовірну особливість «сильної типізації при слабкому зв'язуванні» ні в якому разі не можна втрачати. -
Якого типу
Value
? Знову ж таки, ми не знаємо і не хочемо знати... підійде будь-що, що можна передати уfind
таembrace_back
. Можливо, зараз хтось скаже: «Та ні, ми знаємо тип, це тип значень контейнеру!» Але ні, це зовсім не обов'язково. Важливо, щоб ці типи можна було привести один до одного. Наприклад, може бути кодvector<string> vec; append_unique(vec, "xyzzy");
і в ньому"xyzzy"
буде типуconst char[6]
а неstring
.
Друге, значення, які повертає функція:
- Що є результатом
find
? Якийсь результат, так само як і уbegin(c)
. Але ми не знаємо, що саме є таким результатом, і це не важливо. Якщо буде цікаво, можна глянути у реалізації, але має значення лише те, що цей тип можна порівняти зend(c)
. - Що повертає
empty()
? Та ми навіть не задумуємось. Щось придатне для порівняння, на зразокbool
... не суть, головне, щоб для нього можна було взяти протилежне значення за допомогою!
.
Третє, параметри функцій:
- Який тип отримує
emplace_back
? не знаємо. Може, такий, як уv
, може й ні. Усе одно. Можемо передати тудиv
? Звісно можемо.
І це все лише в одному прикладі. Є багато інших ситуацій, коли ми ігноруємо типи:
-
Четверте, будь-який тимчасовий об'єкт. Якщо нам вже ім'я об'єкту не важливе, тип тим більше не має значення і не вартий того, щоб згадувати його в коді.
-
П'яте, будь-яке використання базового класу. Там суть саме в тому, що ми не знаємо, який саме динамічний клас використовується.
-
Шосте, будь-який виклик віртуального методу. На додачу, цей віртуальний метод теж може повертати результат різних типів, створюючи додаткове коло «неважливого конкретного динамічного типу».
-
Сьоме, будь-яке використання
function<>, bind
або подібного. Просто задумайтесь, наскільки мало ми про це знаємо і наскільки це робить нас щасливими. Візьмемо для прикладу виразfunction<int(string)>
. Ми не тільки не знаємо, якого об'єкту чи функції він стосується, ми навіть уявити не можемо, як саме виглядатиме реалізація, бо приведення типів може відбуватись в усі сторони. Дозволяється повертати будь-що, що приводиться до цілого числа; і приймати будь-який аргумент, який буде конвертуватися в рядок. Більш-менш ясно, що це «щось» приймає рядки й видає числа. Незнання — сила. -
Восьме, будь-яка узагальнена лямбда (C++14 generic lambda function).
Можливо, вдасться навести більше прикладів.
Таким чином, втрата вірності може бути шкідливою в інших сферах життя. Але у програмуванні краще зраджувати звичці використовувати один і той самий тип, бо це дозволяє створювати код, придатний для повторного використання.
Ще немає коментарів