Чому std :: функція не бере участі у вирішенні перевантаження?


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 ++), що і під час якого етапу перекладу змушує вищезгадане не скласти?


1
@Evg, але є лише одне перевантаження, яке має сенс прийняти в прикладі ОП. У вашому прикладі обидва
склали

Відповіді:


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

Розв’язання перевантаження виникає лише тоді, коли (a) ви називаєте ім'я функції / оператора, або (b) передаєте його покажчику (на функцію або функцію члена) з явною підписом.

Ніякого тут не відбувається.

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){}

отримати силу одного типу і дозволити відрахування.


"оскільки baz є перевантаженою функцією, немає єдиного типу, який можна вивести", але оскільки C ++ 14 ", цей конструктор не бере участі у роздільній здатності перевантаження, якщо f не викликається для типів аргументів Args ... і повернення типу R" Я не впевнений, але я був близький, щоб очікувати, що цього буде достатньо для вирішення двозначності
idclev 463035818

3
@ bivlyknownas_463035818 Але для визначення цього потрібно спочатку вивести тип, а він не може, оскільки це перевантажене ім'я.
NathanOliver

1

Після того, як компілятор вирішує, яку перевантаження передати std::functionконструктору, все, що він знає, - це те, що std::functionконструктор має намір прийняти будь-який тип. Він не має можливості спробувати обидві перевантаження і виявити, що перший не компілюється, а другий.

Спосіб вирішити це - явно сказати компілятору, яке перевантаження ви хочете static_cast:

Bar(std::function<void()> fun = static_cast<void(*)()>(baz)) : bazFn(fun){}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.