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

17

Я знаю, что следующий код не скомпилируется.

void baz(int i) { }
void baz() {  }


class Bar
{
    std::function<void()> bazFn;
public:
    Bar(std::function<void()> fun = baz) : bazFn(fun){}

};

int main(int argc, char **argv)
{
    Bar b;
    return 0;
}

Потому std::functionчто сказано не учитывать разрешение перегрузки, как я читал в этом другом посте .

Я не совсем понимаю технические ограничения, которые вынудили такое решение.

Я читал об этапах перевода и шаблонах на cppreference, но я не могу придумать никаких рассуждений, к которым я не смог найти контрпример. Объяснили полу-дилетанту (все еще новичку в C ++), что и на каком этапе трансляции вышеописанное не компилируется?

TuRtoise
источник
1
@Evg, но есть только одна перегрузка, которая будет иметь смысл в примере с OP. В вашем примере оба сделают совпадение
idclev 463035818

Ответы:

13

Это на самом деле не имеет ничего общего с «фазами перевода». Это чисто о конструкторах std::function.

Видите, std::function<R(Args)>не требует, чтобы данная функция была именно такого типа R(Args). В частности, для этого не требуется указатель функции. Он может принимать любой вызываемый тип (указатель на функцию-член, некоторый объект, имеющий перегрузку operator()), при условии, что он вызывается, как если бы он принял Argsпараметры и возвратил что-то конвертируемое в R (или, если Rесть void, он может вернуть все что угодно).

Чтобы сделать это, соответствующий конструктор std::functionдолжен быть шаблон : template<typename F> function(F f);. То есть он может принимать любой тип функции (с учетом вышеуказанных ограничений).

Выражение bazпредставляет набор перегрузки. Если вы используете это выражение для вызова набора перегрузки, это нормально. Если вы используете это выражение в качестве параметра для функции, которая принимает указатель на конкретную функцию, C ++ может уменьшить перегрузку, установленную для одного вызова, что делает его нормальным.

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

Николь Болас
источник
Извините, если я что-то не так понял, но почему в C ++ нет возможности различать доступные перегрузки? Теперь я вижу, что std :: function <T> принимает любой совместимый тип, не только точное совпадение, но и только одна из этих двух перегрузок baz () вызывается так, как если бы она принимала указанные параметры. Почему невозможно устранить неоднозначность?
TuRtoise
Потому что все, что видит C ++ - это подпись, которую я цитировал. Он не знает, с чем он должен совпадать. По сути, всякий раз, когда вы используете набор перегрузки для чего-либо, что не очевидно, что правильный ответ явно из кода C ++ (код, являющийся тем объявлением шаблона), язык заставляет вас изложить то, что вы имеете в виду.
Николь Болас
1
@TuRtoise: параметр шаблона в functionшаблоне класса не имеет значения . Важным является параметр шаблона в конструкторе, который вы вызываете. Который просто typename F: ака, любой тип.
Николь Болас
1
@TuRtoise: я думаю, вы что-то неправильно поняли. Это просто F, потому что так работают шаблоны. Из сигнатуры этот конструктор функции принимает любой тип, поэтому любая попытка вызвать его с вычетом аргумента шаблона будет выводить тип из параметра. Любая попытка применить дедукцию к набору перегрузки является ошибкой компиляции. Компилятор не пытается вывести все возможные типы из набора, чтобы увидеть, какие из них работают.
Николь Болас
1
«Любая попытка применить дедукцию к набору перегрузки является ошибкой компиляции. Компилятор не пытается вывести все возможные типы из набора, чтобы увидеть, какие из них получаются». Именно то, что мне не хватало, спасибо :)
TuRtoise
7

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

Здесь ничего не происходит.

std::functionберет любой объект, который совместим с его подписью. Это не берет указатель функции определенно. (лямбда не является функцией std, а функция std не является лямбда)

Теперь в моих вариантах доморощенных функций для подписи R(Args...)я также принимаю R(*)(Args...)аргумент (точное совпадение) именно по этой причине. Но это означает, что он поднимает сигнатуры «точного соответствия» над «совместимыми» сигнатурами.

Основная проблема заключается в том, что набор перегрузки не является объектом C ++. Вы можете назвать набор перегрузки, но вы не можете передать его «изначально».

Теперь вы можете создать псевдо-перегрузочный набор функции следующим образом:

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

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

это создает один объект C ++, который может выполнять разрешение перегрузки для имени функции.

Расширяя макросы, мы получаем:

[](auto&&...args)
noexcept(noexcept( baz(decltype(args)(args)...) ) )
-> decltype( baz(decltype(args)(args)...) )
{ return baz(decltype(args)(args)...); }

что раздражает писать. Более простая, но чуть менее полезная версия здесь:

[](auto&&...args)->decltype(auto)
{ return baz(decltype(args)(args)...); }

у нас есть лямбда, которая принимает любое количество аргументов, а затем идеально передает их baz.

Затем:

class Bar {
  std::function<void()> bazFn;
public:
  Bar(std::function<void()> fun = OVERLOADS_OF(baz)) : bazFn(fun){}
};

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

По крайней мере, было предложено определить операцию на языке C ++, которая преобразует имя функции в объект набора перегрузки. Пока такое стандартное предложение не входит в стандарт,OVERLOADS_OF макрос полезен.

Вы можете пойти еще дальше и поддержать приведение к совместимой функции-указатель.

struct baz_overloads {
  template<class...Ts>
  auto operator()(Ts&&...ts)const
  RETURNS( baz(std::forward<Ts>(ts)...) );

  template<class R, class...Args>
  using fptr = R(*)(Args...);
  //TODO: SFINAE-friendly support
  template<class R, class...Ts>
  operator fptr<R,Ts...>() const {
    return [](Ts...ts)->R { return baz(std::forward<Ts>(ts)...); };
  }
};

но это начинает становиться тупым.

Живой пример .

#define OVERLOADS_T(...) \
  struct { \
    template<class...Ts> \
    auto operator()(Ts&&...ts)const \
    RETURNS( __VA_ARGS__(std::forward<Ts>(ts)...) ); \
\
    template<class R, class...Args> \
    using fptr = R(*)(Args...); \
\
    template<class R, class...Ts> \
    operator fptr<R,Ts...>() const { \
      return [](Ts...ts)->R { return __VA_ARGS__(std::forward<Ts>(ts)...); }; \
    } \
  }
Якк - Адам Невраумонт
источник
5

Проблема здесь заключается в том, что компилятору ничего не говорится о том, как сделать функцию для затухания указателя. Если у тебя есть

void baz(int i) { }
void baz() {  }

class Bar
{
    void (*bazFn)();
public:
    Bar(void(*fun)() = baz) : bazFn(fun){}

};

int main(int argc, char **argv)
{
    Bar b;
    return 0;
}

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

При использовании std::functionвы вызываете его конструктор объекта функции, который имеет форму

template< class F >
function( F f );

и так как это шаблон, он должен определить тип передаваемого объекта. Поскольку bazэто перегруженная функция, нет единого типа, который можно вывести, поэтому выведение шаблона завершится неудачно, и вы получите ошибку. Вы должны использовать

Bar(std::function<void()> fun = (void(*)())baz) : bazFn(fun){}

получить силу одного типа и разрешить удержание.

NathanOliver
источник
«поскольку baz является перегруженной функцией, нет единственного типа, который можно вывести», но, поскольку C ++ 14 «, этот конструктор не участвует в разрешении перегрузки, если только f не может вызываться для аргументов типов Args ... и возвращать тип R». не уверен, но я был близок к тому, чтобы ожидать, что этого будет достаточно, чтобы решить эту двусмысленность
idclev 463035818
3
@ beforelyknownas_463035818 Но чтобы определить это, сначала нужно вывести тип, а это невозможно, поскольку это перегруженное имя.
Натан Оливер
1

В тот момент, когда компилятор решает, какую перегрузку передать в std::functionконструктор, все, что он знает, это то, что std::functionконструктор имеет шаблон любого типа. У него нет возможности попробовать обе перегрузки и обнаружить, что первый не компилируется, а второй - компилируется.

Чтобы решить эту проблему, нужно явно указать компилятору, какую перегрузку вы хотите, с помощью static_cast:

Bar(std::function<void()> fun = static_cast<void(*)()>(baz)) : bazFn(fun){}
Алан Биртлз
источник