Коли я повинен використовувати автоматичний відрахування типу C ++ 14?


144

З випуском GCC 4.8.0 у нас є компілятор, який підтримує автоматичне відрахування типу повернення, що є частиною C ++ 14. З -std=c++1y, я можу зробити це:

auto foo() { //deduced to be int
    return 5;
}

Моє запитання: Коли я повинен використовувати цю функцію? Коли це потрібно і коли це робить код чистішим?

Сценарій 1

Перший сценарій, про який я можу придумати, - це коли це можливо. Кожна функція, яка може бути записана таким чином, повинна бути. Проблема в цьому полягає в тому, що він не завжди може зробити код більш читабельним.

Сценарій 2

Наступний сценарій - уникати більш складних типів повернення. Як дуже легкий приклад:

template<typename T, typename U>
auto add(T t, U u) { //almost deduced as decltype(t + u): decltype(auto) would
    return t + u;
}

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

Сценарій 3

Далі, щоб запобігти надмірності:

auto foo() {
    std::vector<std::map<std::pair<int, double>, int>> ret;
    //fill ret in with stuff
    return ret;
}

У C ++ 11 ми можемо іноді просто return {5, 6, 7};замість вектора, але це не завжди виходить, і нам потрібно вказати тип і в заголовку функції, і в тілі функції. Це суто зайве, і автоматичне відрахування типу повернення рятує нас від цієї надмірності.

Сценарій 4

Нарешті, його можна використовувати замість дуже простих функцій:

auto position() {
    return pos_;
}

auto area() {
    return length_ * width_;
}

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

Висновок

З викладеними сценаріями, який із них насправді виявляється ситуацією, коли ця функція корисна для того, щоб зробити код чистішим? Що про сценарії, які я тут нехтував згадувати? Які заходи обережності слід вжити, перш ніж використовувати цю функцію, щоб вона не кусала мене згодом? Чи є щось нове, що ця функція приносить до столу, що без неї неможливо?

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


18
Чудове запитання! Хоча ви запитуєте, які сценарії роблять код "кращим", мені також цікаво, які сценарії можуть зробити його гіршим .
Дрю Дорманн

2
@DrewDormann, це мені теж цікаво. Мені подобається використовувати нові функції, але дуже важливо знати, коли ними користуватися, а коли не потрібно. Настає період часу, коли з’являються нові функції, які ми вирішимо розібратися в цьому, тому давайте зробимо це зараз, щоб ми були готові до того, коли це буде офіційно :)
chris

2
@ NicolBolas, Мабуть, але того, що він зараз знаходиться у фактичному випуску компілятора, було б достатньо, щоб люди почали використовувати його в особистому коді (напевно, це потрібно уникати від проектів на даний момент). Я з тих людей, хто любить використовувати новітні можливі функції у власному коді, і хоча я не знаю, наскільки добре йде пропозиція з комітетом, я вважаю, що це перший включений у цей новий варіант говорить щось. Це може бути краще залишити на потім, або (я не знаю, наскільки це добре би спрацювало) відродилося, коли ми точно знаємо, що воно настане.
chris

1
@NicolBolas, Якщо це допоможе, його прийнято зараз: p
chris

1
Поточні відповіді, схоже, не згадують, що заміна ->decltype(t+u)автоматичного відрахування вбиває SFINAE.
Марк Глісс

Відповіді:


62

C ++ 11 викликає подібні питання: коли використовувати відрахування типу повернення в лямбдах і коли використовувати autoзмінні.

Традиційна відповідь на запитання в C і C ++ 03 була "через межі операторів, ми робимо типи явними, в виразах вони, як правило, неявні, але ми можемо зробити їх явними за допомогою касти". C ++ 11 і C ++ 1y вводять інструменти відрахування типів, щоб ви могли залишити тип у нових місцях.

Вибачте, але ви не збираєтеся вирішувати це наперед, викладаючи загальні правила. Вам потрібно переглянути конкретний код і вирішити для себе, чи сприяє читаемості вказівка ​​типів у всіх місцях: чи краще для вашого коду сказати: "тип цієї речі - X", чи краще для у вашому коді, щоб сказати, "тип цієї речі не має значення для розуміння цієї частини коду: компілятор повинен знати, і ми, ймовірно, можемо це опрацювати, але нам не потрібно цього говорити"?

Оскільки "читабельність" об'єктивно не визначена [*], і, крім того, вона залежить від читача, ви несете відповідальність як автор / редактор фрагмента коду, який не може повністю задовольнити керівництво по стилю. Навіть у тій мірі, в якій посібник зі стилів визначає норми, різні люди віддають перевагу різним нормам і будуть схильні вважати що-небудь незнайоме як "менш читабельне". Тож про читанні певного запропонованого правила стилю часто можна судити лише в контексті інших діючих правил стилю.

Усі ваші сценарії (навіть перший) знайдуть користь для когось стилю кодування. Особисто я вважаю, що другий є найбільш переконливим випадком використання, але навіть я припускаю, що це залежатиме від ваших інструментів документації. Не дуже корисно бачити документально підтверджений тип повернення шаблону функції auto, тоді як, бачачи це документально, як decltype(t+u)створює опублікований інтерфейс, на який можна (сподіваємось) покластися.

[*] Інколи хтось намагається зробити якісь об'єктивні вимірювання. У тій мірі, коли хтось коли-небудь придумує якісь статистично значущі та загальноприйняті результати, вони повністю ігноруються робочими програмістами на користь авторських інстинктів того, що «читабельно».


1
Хороший момент із з'єднаннями з лямбдами (хоча це дозволяє отримати більш складні функції функціонування). Головне, що це більш нова особливість, і я все ще намагаюся збалансувати плюси і мінуси кожного випадку використання. Для цього корисно побачити причини, чому це було б використано для чого, щоб я сам виявив, чому я віддаю перевагу тому, що роблю. Я можу це переосмислити, але я такий.
chris

1
@chris: як я кажу, я думаю, що все зводиться до одного і того ж. Чи краще для вашого коду сказати: "тип цієї речі - X", чи краще, щоб ваш код сказав: "тип цієї речі не має значення, компілятор повинен знати, і ми могли б, мабуть, розробити це" але нам не потрібно цього говорити ". Коли ми пишемо, 1.0 + 27Uми стверджуємо останнє, коли пишемо, (double)1.0 + (double)27Uми стверджуємо перше. Простота функції, ступінь дублювання, уникання цього decltypeможуть сприяти цьому, але жодна не буде надійно визначальною.
Стів Джессоп

1
Чи краще для вашого коду сказати: "тип цієї речі - X", чи краще, щоб ваш код сказав: "тип цієї речі не має значення, компілятор повинен знати, і ми могли б, можливо, це розробити" але нам не потрібно цього говорити ". - Це речення саме уздовж того, що я шукаю. Я буду враховувати це, коли я стикаюся з варіантами використання цієї функції та autoзагалом.
chris

1
Я хотів би додати, що IDE можуть полегшити "проблему читабельності". Візьмемо, наприклад, Visual Studio: Якщо навести курсор на autoключове слово, воно відобразить фактичний тип повернення.
andreee

1
@andreee: це правда в межах. Якщо тип має багато псевдонімів, то пізнання фактичного типу не завжди є таким корисним, як ви б сподівалися. Наприклад, тип ітератора може бути int*, але що насправді важливо, якщо що, це причина в int*тому, що це те, що std::vector<int>::iterator_typeє вашими поточними параметрами збірки!
Стів Джессоп

30

Взагалі кажучи, тип повернення функції дуже допомагає документувати функцію. Користувач буде знати, що очікується. Однак є один випадок, коли я думаю, що було б непогано скинути цей тип повернення, щоб уникнути надмірності. Ось приклад:

template<typename F, typename Tuple, int... I>
  auto
  apply_(F&& f, Tuple&& args, int_seq<I...>) ->
  decltype(std::forward<F>(f)(std::get<I>(std::forward<Tuple>(args))...))
  {
    return std::forward<F>(f)(std::get<I>(std::forward<Tuple>(args))...);
  }

template<typename F, typename Tuple,
         typename Indices = make_int_seq<std::tuple_size<Tuple>::value>>
  auto
  apply(F&& f, Tuple&& args) ->
  decltype(apply_(std::forward<F>(f), std::forward<Tuple>(args), Indices()))
  {
    return apply_(std::forward<F>(f), std::forward<Tuple>(args), Indices());
  }

Цей приклад взято з офіційного документу комітету N3493 . Мета функції apply- переслати елементи а std::tupleдо функції та повернути результат. int_seqІ make_int_seqє лише частиною реалізації, і, ймовірно , тільки заплутати будь-який користувач , намагаючись зрозуміти , що він робить.

Як бачите, тип повернення - це не що інше, як decltypeповернений вираз. Більше того, apply_не будучи призначеним для того, щоб його бачили користувачі, я не впевнений у корисності документування його типу повернення, коли він більш-менш збігається applyз таким. Я думаю, що в цьому конкретному випадку відміна типу повернення робить функцію більш читаною. Зауважте, що цей самий тип повернення був фактично відхилений і замінений decltype(auto)на пропозицію додати applyдо стандарту N3915 (також зауважте, що моя оригінальна відповідь передує цій роботі):

template <typename F, typename Tuple, size_t... I>
decltype(auto) apply_impl(F&& f, Tuple&& t, index_sequence<I...>) {
    return forward<F>(f)(get<I>(forward<Tuple>(t))...);
}

template <typename F, typename Tuple>
decltype(auto) apply(F&& f, Tuple&& t) {
    using Indices = make_index_sequence<tuple_size<decay_t<Tuple>>::value>;
    return apply_impl(forward<F>(f), forward<Tuple>(t), Indices{});
}

Однак більшу частину часу краще зберігати цей тип повернення. У конкретному випадку, який я описав вище, тип повернення є досить нечитабельним, і потенційний користувач нічого не отримає від цього. Гарна документація з прикладами буде набагато кориснішою.


Ще одна річ, про яку ще не було сказано: хоча declype(t+u)дозволяє використовувати вираз SFINAE , decltype(auto)це не робить (хоча є пропозиція змінити цю поведінку). Візьмемо для прикладу foobarфункцію, яка викличе fooфункцію члена типу, якщо вона існує, або викличе barфункцію члена типу, якщо вона існує, і припустимо, що клас завжди має точність fooабо barоба обидва відразу:

struct X
{
    void foo() const { std::cout << "foo\n"; }
};

struct Y
{
    void bar() const { std::cout << "bar\n"; }
};

template<typename C> 
auto foobar(const C& c) -> decltype(c.foo())
{
    return c.foo();
}

template<typename C> 
auto foobar(const C& c) -> decltype(c.bar())
{
    return c.bar();
}

Виклик foobarна прикладі Xбуде відображатися fooпри виклику foobarна екземплярі Yбуде відображатися bar. Якщо ви замість цього використовуєте автоматичне відрахування типу повернення (з або без decltype(auto)), ви не отримаєте вираження SFINAE і виклик foobarекземпляра будь-якого Xабо Yпризведе до помилки часу компіляції.


8

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

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


1
Я погоджуюся, що люди з різним походженням з цього приводу мають різні думки, але я сподіваюся, що це прийде до C ++ - висновку. У (майже) кожному випадку недоцільно зловживати мовними функціями, намагаючись перетворити його в інший, наприклад, за допомогою #defines для перетворення C ++ у VB. Функції, як правило, мають хороший консенсус у розумінні мови щодо правильного використання, а що ні, відповідно до того, до чого звикли програмісти цієї мови. Одна і та ж функція може бути присутнім на декількох мовах, але кожна має свої вказівки щодо її використання.
chris

2
Деякі функції мають добрий консенсус. Багато хто ні. Я знаю багато програмістів, які вважають, що більшість Boost - це сміття, яке не слід використовувати. Я також знаю тих, хто вважає, що найбільше це трапляється на C ++. В будь-якому випадку я думаю, що на цьому слід вести кілька цікавих дискусій, але це дійсно точний приклад близького та не конструктивного варіанту на цьому сайті.
Габе Сечан

2
Ні, я з вами повністю не згоден, і я думаю, що autoце чисте благо. Це прибирає багато зайвих. Повторювати тип повернення іноді просто біль. Якщо ви хочете повернути лямбда, це може бути навіть неможливим без збереження результату в значку std::function, що може спричинити за собою накладні витрати.
Yongwei Wu

1
Принаймні одна ситуація, коли це все, але цілком необхідно. Припустимо, вам потрібно зателефонувати за функціями, а потім записати результати перед їх поверненням, а функції не обов'язково мають усі типи повернення. Якщо ви робите це через функцію реєстрації, яка приймає функції та їх аргументи як параметри, вам потрібно буде оголосити тип повернення функції журналу, autoщоб він завжди відповідав типу повернення переданої функції.
Час Джастіна - Відновіть Моніку

1
[Технічно є інший спосіб зробити це, але він використовує магію шаблонів, щоб зробити в основному те саме, і все ще потрібна функція ведення журналу для того, autoщоб відстежувати тип повернення.]
Justin Time - Відновіть Моніку

7

Це не має нічого спільного з простотою функції (як тепер передбачається видалений дублікат цього питання).

Або тип повернення є фіксованим (не використовуйте auto), або комплексним чином залежить від параметра шаблону (використання autoв більшості випадків поєднується з decltypeкількома точками повернення).


3

Розглянемо реальне виробниче середовище: багато функцій та тестових блоків все взаємозалежно від типу повернення foo(). Тепер припустимо, що тип повернення потрібно змінити з будь-якої причини.

Якщо тип повернення є autoскрізь, а абоненти foo()та пов'язані з ним функції використовують autoпри отриманні повернутого значення, зміни, які потрібно внести, будуть мінімальними. Якщо ні, це може означати години надзвичайно виснажливої ​​та схильної до помилок роботи.

Як приклад у реальному світі, мене попросили змінити модуль із використання повних покажчиків скрізь на розумні покажчики. Виправлення одиничних тестів було більш болючим, ніж власне код.

Хоча існують й інші способи впоратися з цим, використання autoповернених типів здається непоганим.


1
У тих випадках не було б краще писати decltype(foo())?
Oren S

Особисто я вважаю, typedefщо декларації s та псевдоніму (тобто usingдекларація типу) в цих випадках краще, особливо коли вони класуються за класом.
MAChitgarha

3

Я хочу навести приклад, коли автоматичний тип повернення ідеальний:

Уявіть, що ви хочете створити короткий псевдонім для довгого наступного виклику функції. За допомогою автоматичного режиму вам не потрібно дбати про вихідний тип повернення (можливо, це зміниться в майбутньому), і користувач може натиснути на оригінальну функцію, щоб отримати реальний тип повернення:

inline auto CreateEntity() { return GetContext()->GetEntityManager()->CreateEntity(); }

PS: Залежить від цього питання.


2

У сценарії 3 я перетворив би тип повернення функції підпису з локальною змінною, яку потрібно повернути. Це зробило б зрозумілішим для клієнтських програмістів, які повертають функцію. Подобається це:

Сценарій 3 Для запобігання надмірності:

std::vector<std::map<std::pair<int, double>, int>> foo() {
    decltype(foo()) ret;
    return ret;
}

Так, у нього немає автоматичного ключового слова, але головне - те саме, щоб запобігти надмірності та полегшити час програмістам, які не мають доступу до джерела.


2
ІМО кращим рішенням для цього є надання доменного імені для концепції того, що має бути представлено у вигляді а, vector<map<pair<int,double>,int>а потім використовувати це.
davidbak
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.