SFINAE на функции с параметрами по умолчанию - свободная функция против оператора () - PullRequest
0 голосов
/ 17 сентября 2018

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

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

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

Ответы [ 2 ]

0 голосов
/ 17 сентября 2018

Имя функции не является именем объекта 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 аргумента.

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

0 голосов
/ 17 сентября 2018

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...