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

template <typename F>
auto func(F f) -> decltype(f(42))
{
    int a = 51;
    return f(51);
}

template <typename F>
auto func(F f) -> decltype(f(42, 42))
{
    int a = 0;
    int b = 10;
    return f(a, b);
}

int defaultFree(int a, int b = 0)
{
    return a;
}

auto defaultLambda = [](int a, int b = 0)
{
    return a;
};

int foo()
{
    return func(defaultFree);  
    //return func(defaultLambda);
}

Годболт ссылка

Приведенная выше версия func(defaultFree) компилируется, в то время как доступны оба шаблона func. Как и ожидалось, он выбирает второй, потому что параметры по умолчанию не считаются частью сигнатуры функции. Действительно, удаление второго шаблона func вызывает ошибку компиляции.

Однако func(defaultLambda) не компилируется из-за двусмысленности: оба { {X1}} шаблоны совпадают. Удаление любого из них приводит к компиляции этой версии.

(То же самое происходит, если вы вручную напишите struct с похожим operator(), конечно. Последние gcc, clang и MSVC все согласны с это тоже.)

По какой причине параметр по умолчанию рассматривается в неоцененном контексте SFINAE для operator(), но не для бесплатной функции?

19
Max Langhof 17 Сен 2018 в 17:43

2 ответа

Лучший ответ

Имя функции - это не имя объекта C ++.

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

Аргументы функции по умолчанию являются частью разрешения перегрузки. Они никогда не передаются как часть типа указателя функции.

Вы можете создать простую оболочку, которая превращает имя функции в объект функции:

#define RETURNS(...) \
  noexcept(noexcept(__VA_ARGS__)) \
  -> decltype(__VA_ARGS__) \
  { return __VA_ARGS__; }

#define OVERLOADS_OF(...) \
  [](auto&&...args) \
  RETURNS( __VA_ARGS__( decltype(args)(args)... ) )

С этим вы можете изменить свой код:

return func(OVERLOADS_OF(defaultFree));

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

Теперь OVERLOADS_OF(defaultFree) - это функциональный объект, который SFINAE проверяет, могут ли его аргументы быть переданы вызываемому объекту с именем defaultFree. Это позволяет передать 1 аргумент.

Функциональные объекты не являются функциями. Лямбда - это объект функции, как и тип возвращаемого значения OVERLOADS_OF. Функциональные объекты могут передаваться и учитываться их перегруженность operator(); они могут запоминать свои параметры по умолчанию, выполнять SFINAE и т. д.

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


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

Один из подходов - использовать ...:

namespace impl {
  template <typename F>
  auto func(F f,...) -> decltype(f(42))
  {
    int a = 51;
    return f(51);
  }

  template <typename F>
  auto func(F f, int) -> decltype(f(42, 42))
  {
    int a = 0;
    int b = 10;
    return f(a, b);
  }
}
template <typename F>
auto func(F f) -> decltype( impl::func(f, 0) )
{
  return impl::func(f, 0);
}

Уловка в том, что int предпочтительнее ..., когда вы передаете 0.

Вы также можете быть более явным и сгенерировать такие черты, как «можно вызывать с 1 аргументом», «можно вызывать с 2 аргументами», а затем указать, что случай 1 аргумент включен только тогда, когда вы можете вызывать с 1, но не с 2 аргументами ,

Существуют также методы упорядочивания разрешения перегрузки на основе диспетчеризации тегов.

9
Yakk - Adam Nevraumont 17 Сен 2018 в 15:14

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

Тип лямбды не подвергается такой настройке. Неоцененное выражение должно включать разрешение перегрузки для operator(), и это находит объявление с аргументом по умолчанию, что позволяет использовать его в вызове с одним аргументом.

Когда лямбда без захвата принудительно преобразовывается в указатель функции (например, func(+defaultLambda);, любезно предоставлено @YSC), двусмысленность исчезает по той же причине.

17
StoryTeller - Unslander Monica 17 Сен 2018 в 15:13