Подтвердить что ты не робот

Почему тип автоматического возврата изменяет разрешение перегрузки?

Благодаря decltype как возвращаемому типу, С++ 11 чрезвычайно упростил внедрение декораторов. Например, рассмотрим этот класс:

struct base
{
  void fun(unsigned) {}
};

Я хочу украсить его дополнительными функциями, и, поскольку я буду делать это несколько раз с различными видами украшений, я сначала представляю класс decorator, который просто перенаправляет все на base. В реальном коде это делается с помощью std::shared_ptr, чтобы я мог удалить декорации и восстановить "голый" объект, и все шаблоны.

#include <utility> // std::forward
struct decorator
{
  base b;

  template <typename... Args>
  auto
  fun(Args&&... args)
    -> decltype(b.fun(std::forward<Args>(args)...))
  {
    return b.fun(std::forward<Args>(args)...);
  }
};

Отличная переадресация и decltype просто замечательные. В реальном коде я фактически использую макрос, которому просто нужно имя функции, все остальное является шаблоном.

И затем я могу ввести класс derived, который добавляет функции моему объекту (derived является неправильным, согласованным, но он помогает понять, что derived есть вид base, хотя и не через наследование).

struct foo_t {};
struct derived : decorator
{
  using decorator::fun; // I want "native" fun, and decorated fun.
  void fun(const foo_t&) {}
};

int main()
{
  derived d;
  d.fun(foo_t{});
}

Затем появился С++ 14 с выводом типа возвращаемого типа, который позволяет писать вещи проще: удалите часть decltype функции пересылки:

struct decorator
{
  base b;

  template <typename... Args>
  auto
  fun(Args&&... args)
  {
    return b.fun(std::forward<Args>(args)...);
  }
};

И потом он ломается. Да, по крайней мере, согласно GCC и Clang, это:

  template <typename... Args>
  auto
  fun(Args&&... args)
    -> decltype(b.fun(std::forward<Args>(args)...))
  {
    return b.fun(std::forward<Args>(args)...);
  }
};

не эквивалентен этому (и проблема не auto vs. decltype(auto)):

  template <typename... Args>
  auto
  fun(Args&&... args)
  {
    return b.fun(std::forward<Args>(args)...);
  }
};

Разрешение перегрузки кажется совершенно другим, и оно заканчивается следующим образом:

clang++-mp-3.5 -std=c++1y main.cc
main.cc:19:18: error: no viable conversion from 'foo_t' to 'unsigned int'
    return b.fun(std::forward<Args>(args)...);
                 ^~~~~~~~~~~~~~~~~~~~~~~~
main.cc:32:5: note: in instantiation of function template specialization
      'decorator::fun<foo_t>' requested here
  d.fun(foo_t{});
    ^
main.cc:7:20: note: passing argument to parameter here
  void fun(unsigned) {}
                   ^

Я понимаю неудачу: мой вызов (d.fun(foo_t{})) отлично не совпадает с сигнатурой derived::fun, которая принимает const foo_t&, поэтому очень нетерпеливый decorator::fun запускается (мы знаем, как Args&&... крайне не терпит привязки ко всему, что не идеально подходит). Поэтому он пересылает это значение в base::fun, которое не может иметь дело с foo_t.

Если я изменю derived::fun, чтобы взять foo_t вместо const foo_t&, то он работает так, как ожидалось, что показывает, что в действительности проблема заключается в том, что существует конкуренция между derived::fun и decorator::fun.

Однако почему черт делает это с возвратом типа вывода? И точнее, почему это поведение было выбрано комитетом?

Чтобы сделать вещи проще, на Coliru:

Спасибо!

4b9b3361

Ответ 1

Просто посмотрите на этот вызов:

d.fun(foo_t{});

Вы создаете временное (i.e значение rvalue), передавая его как аргумент функции. Как вы думаете, что происходит?

  • Сначала он пытается связать с аргументом Arg&&, так как он может принимать значение rvalue , но из-за недопустимого вывода типа возврата (который снова из-за foo_t не может преобразовываться в unsigned int > , из-за которого b.fun(std::forward<Args>(args)...) оказывается недопустимым выражением), эта функция отклоняется, если вы используете тип возвращаемого типа decltype(expr), так как в этом случае SFINAE входит в изображение. Но если вы просто используете auto, то SFINAE не попадает в изображение, и ошибка классифицируется как жесткая ошибка, которая приводит к сбою компиляции.

  • Вторая перегрузка, принимающая foo_t const& как аргумент, вызывается, если SFINAE работает в первом случае.