Когда следует использовать автоматический возврат типа С++ 14?

С выпуском GCC 4.8.0 у нас есть компилятор, который поддерживает автоматический вывод типа возвращаемого типа, часть С++ 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;
}

В С++ 11 вместо вектора мы можем просто просто return {5, 6, 7};, но это не всегда получается, и нам нужно указать тип как в заголовке функции, так и в теле функции. Это чисто избыточно, и автоматический возврат типа возврата избавляет нас от этой избыточности.

Сценарий 4

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

auto position() {
    return pos_;
}

auto area() {
    return length_ * width_;
}

Иногда, однако, мы можем посмотреть на функцию, желая узнать точный тип, и если она там не указана, нам нужно перейти к другой точке кода, например, когда объявлен pos_.

Заключение

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

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

Ответ 1

С++ 11 вызывает похожие вопросы: когда использовать вывод типа return в lambdas и когда использовать переменные auto.

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

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

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

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

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

Ответ 2

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

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. Я думаю, что в этом конкретном случае сброс возвращаемого типа делает функцию более читаемой. Обратите внимание, что этот очень возвращаемый тип фактически был сброшен и заменен на 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{});
}

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


Еще одна вещь, о которой еще не упоминалось: while declype(t+u) позволяет использовать выражение есть предложение изменить это поведение). Возьмем, например, функцию 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 приведет к ошибке времени компиляции.

Ответ 3

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

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

Ответ 4

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

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

Ответ 5

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

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

Как пример в реальном мире, меня попросили изменить модуль с помощью простых указателей повсюду на интеллектуальные указатели. Фиксация модульных тестов была более болезненной, чем фактический код.

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

Ответ 6

Я хочу привести пример, где автоматический тип возврата:

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

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

PS: Зависит от этого вопроса.