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

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

Ссылка Godbolt

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

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

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

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

Ответ 1

Имя функции не является именем объекта 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 аргументом", "можно вызвать с двумя аргументами", а затем указать, что аргумент 1 аргумента разрешен только тогда, когда вы можете вызывать с 1, но не с двумя аргументами,

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

Ответ 2

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

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

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