Когда следует использовать автоматическое определение типа возвращаемого значения C ++ 14?

146

С выпущенным 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_объявлено.

Вывод

С учетом этих сценариев, в каких из них на самом деле окажется ситуация, когда эта функция будет полезна для очистки кода? А как насчет сценариев, которые я здесь не упомянул? Какие меры предосторожности следует предпринять перед использованием этой функции, чтобы она меня не укусила позже? Есть ли что-то новое в этой функции, что невозможно без нее?

Обратите внимание, что несколько вопросов предназначены для помощи в поиске точек зрения, с которых можно на них ответить.

Крис
источник
19
Замечательный вопрос! Пока вы спрашиваете, какие сценарии делают код «лучше», мне также интересно, какие сценарии сделают его хуже .
Дрю Дорманн
2
@DrewDormann, вот и мне интересно. Мне нравится использовать новые функции, но очень важно знать, когда их использовать, а когда нет. Есть период времени, когда появляются новые функции, которые мы используем, чтобы разобраться в этом, поэтому давайте сделаем это сейчас, чтобы мы были готовы, когда это появится официально :)
Крис
2
@NicolBolas, возможно, но того факта, что он сейчас находится в актуальном выпуске компилятора, было бы достаточно, чтобы люди начали использовать его в личном коде (на данном этапе его определенно нужно держать подальше от проектов). Я один из тех людей, которым нравится использовать новейшие возможные функции в моем собственном коде, и хотя я не знаю, насколько хорошо это предложение работает с комитетом, я полагаю, что тот факт, что он первым включен в этот новый вариант, говорит о том, что что нибудь. Возможно, его лучше оставить на потом или (я не знаю, насколько хорошо это будет работать) возродить, когда мы будем знать наверняка, что это произойдет.
Крис
1
@NicolBolas, если поможет, то сейчас принято: p
chris
1
В текущих ответах, похоже, не упоминается, что замена ->decltype(t+u)автоматическим вычетом убивает SFINAE.
Marc Glisse

Ответы:

63

С ++ 11 вызывает аналогичные вопросы: когда использовать вывод типа возвращаемого значения в лямбдах, а когда использовать auto переменные.

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

Извините, но вы не собираетесь решать эту проблему заранее, устанавливая общие правила. Вам нужно взглянуть на конкретный код и решить для себя, помогает ли он читабельности указывать типы повсюду: лучше ли для вашего кода указывать «тип этой вещи - X» или лучше для ваш код, чтобы сказать: «Тип этой вещи не имеет отношения к пониманию этой части кода: компилятор должен знать, и мы, вероятно, могли бы это решить, но нам не нужно говорить об этом здесь»?

Поскольку «читабельность» не определяется объективно [*] и, кроме того, зависит от читателя, вы несете ответственность как автор / редактор фрагмента кода, который не может быть полностью удовлетворен руководством по стилю. Даже в той степени, в которой руководство по стилю определяет нормы, разные люди предпочтут разные нормы и будут склонны находить все незнакомое «менее читаемым». Таким образом, удобочитаемость конкретного предложенного правила стиля часто можно оценить только в контексте других действующих правил стиля.

Все ваши сценарии (даже первый) найдут применение чьему-то стилю кодирования. Лично я считаю, что второй вариант использования является наиболее убедительным, но даже в этом случае я ожидаю, что он будет зависеть от ваших инструментов документации. Не очень полезно документировать, что тип возвращаемого значения шаблона функции - это auto, в то время как его документирование decltype(t+u)создает опубликованный интерфейс, на который можно (надеюсь) положиться.

[*] Иногда кто-то пытается сделать какие-то объективные измерения. В той небольшой степени, в которой кто-либо когда-либо дает какие-либо статистически значимые и общеприменимые результаты, они полностью игнорируются работающими программистами в пользу авторских инстинктов того, что «читабельно».

Стив Джессоп
источник
1
Хорошая точка зрения на связи с лямбдами (хотя это позволяет создавать более сложные тела функций). Главное, что это более новая функция, и я все еще пытаюсь сбалансировать плюсы и минусы каждого варианта использования. Для этого полезно увидеть причины, по которым он будет использоваться для чего, чтобы я сам мог понять, почему я предпочитаю то, что делаю. Возможно, я слишком много об этом думаю, но я такой.
Крис
1
@chris: как я уже сказал, я думаю, все сводится к одному и тому же. Что лучше для вашего кода, чтобы сказать: «Тип этой вещи - X», или лучше, чтобы ваш код сказал: «Тип этой вещи не имеет значения, компилятор должен знать, и мы, вероятно, могли бы это решить но нам не нужно это говорить ». Когда мы пишем, 1.0 + 27Uмы утверждаем последнее, когда мы пишем, (double)1.0 + (double)27Uмы утверждаем первое. Простота функции, степень дублирования, избегание - decltypeвсе это может способствовать этому, но ни один из них не будет надежно решающим.
Стив Джессоп
1
Что лучше для вашего кода, чтобы сказать: «Тип этой вещи - X», или лучше, чтобы ваш код сказал: «Тип этой вещи не имеет значения, компилятор должен знать, и мы, вероятно, могли бы это решить но нам не нужно это говорить ». - Это предложение в точности соответствует тому, что я ищу. Я буду учитывать это, когда буду сталкиваться с вариантами использования этой функции и autoв целом.
Крис
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- переадресовать элементы a std::tupleфункции и вернуть результат. int_seqиmake_int_seq являются лишь частью реализации, и, вероятно , только запутать любой пользователь , пытаясь понять , что он делает.

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

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{});
}

Однако в большинстве случаев лучше сохранить этот возвращаемый тип. В конкретном случае, который я описал выше, тип возвращаемого значения довольно нечитабелен, и потенциальный пользователь ничего не получит от его знания. Намного полезнее будет хорошая документация с примерами.


Еще одна вещь, о которой еще не упоминалось: while 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вызовет ошибку времени компиляции.

Морвенн
источник
10

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

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

Гейб Сечан
источник
1
Я согласен с тем, что люди с разным опытом будут иметь разные мнения по этому поводу, но я надеюсь, что это приведет к выводу, подобному C ++. В (почти) каждом случае непростительно злоупотреблять возможностями языка, пытаясь превратить его в другой, например, используя #defines для превращения C ++ в VB. Функции обычно имеют хороший консенсус в мировоззрении языка о том, что правильно, а что нет, в зависимости от того, к чему привыкли программисты этого языка. Одна и та же функция может присутствовать на нескольких языках, но для каждого из них существуют свои правила ее использования.
Крис
2
Некоторые функции имеют хороший консенсус. Многие этого не делают. Я знаю многих программистов, которые думают, что большая часть Boost - это мусор, который не следует использовать. Я также знаю некоторых, кто считает, что это самое лучшее, что случилось с C ++. В любом случае я думаю, что по этому поводу можно провести несколько интересных обсуждений, но это действительно точный пример того, что на этом сайте нет конструктивного варианта.
Гейб Сечан
2
Нет, я полностью с вами не согласен и считаю autoэто чистым благословением. Это убирает много избыточности. Иногда просто неудобно повторять возвращаемый тип. Если вы хотите вернуть лямбду, это может быть даже невозможно без сохранения результата в a std::function, что может повлечь за собой некоторые накладные расходы.
Yongwei Wu
1
Есть по крайней мере одна ситуация, когда это совершенно необходимо. Предположим, вам нужно вызвать функции, а затем записать результаты перед их возвратом, и не обязательно, чтобы все функции имели один и тот же тип возвращаемого значения. Если вы делаете это с помощью функции ведения журнала, которая принимает функции и их аргументы в качестве параметров, вам необходимо объявить тип возвращаемого значения функции ведения журнала, autoчтобы он всегда соответствовал типу возвращаемого значения переданной функции.
Джастин Тайм - Восстановить Монику
1
[Технически есть другой способ сделать это, но он использует магию шаблонов, чтобы делать в основном то же самое, и по-прежнему требует, чтобы функция регистрации была autoдля конечного возвращаемого типа.]
Джастин Тайм - Восстановить Монику
8

Это не имеет ничего общего с простотой функции (как предполагается, теперь удаленная копия этого вопроса).

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

Бен Фойгт
источник
Да, auto следует использовать, если тип неопределен / переменный (обычно из-за шаблонов). Если тип известен, положите эту вещь. Если эта вещь неуклюжая или неудобно большая, используйте typedef / using.
Энди Кроувел,
3

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

Если тип возвращаемого значения присутствует autoвезде, а вызывающие foo()и связанные функции используют autoпри получении возвращаемого значения, изменения, которые необходимо внести, минимальны. В противном случае это может означать часы чрезвычайно утомительной и подверженной ошибкам работы.

В качестве реального примера меня попросили изменить модуль с использования повсюду необработанных указателей на интеллектуальные указатели. Исправление модульных тестов было более болезненным, чем сам код.

Хотя есть и другие способы решения этой проблемы, использование autoвозвращаемых типов кажется подходящим вариантом.

Джим Вуд
источник
1
О тех случаях, не лучше ли было бы написать decltype(foo())?
Oren S
Лично я считаю, что объявления typedefs и alias (то есть usingобъявление типа) лучше в этих случаях, особенно когда они привязаны к классу.
МАЧитгарха
3

Я хочу привести пример, в котором тип возврата auto идеален:

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

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;
}

Да, у него нет ключевого слова auto, но принцип тот же, чтобы предотвратить избыточность и облегчить программистам, у которых нет доступа к исходному тексту.

Подавать Лаурийссен
источник
3
ИМО, лучшее решение для этого - предоставить доменное имя для концепции того, что должно быть представлено как, vector<map<pair<int,double>,int>а затем использовать это.
davidbak