Pull to refresh

Помощь компилятору С++ в разрешении перегрузки функций

Reading time4 min
Views4.1K
В некоторых случаях компиляторы С++ не могут выбрать подходящую перегружаемую функцию, например, в очевидной с человеческой точки зрения ситуации — возникает ошибка компиляции:

void f(int i){}
void f(string s){}

vector<int> int_c = { 1, 2, 3, 4, 5 };
vector<string> string_c = { "Sic" ,"transit" ,"gloria" ,"mundi" };
for_each(begin(int_c), end(int_c), f);//error C2672: "for_each": не найдена соответствующая перегруженная функция

Проблема – в отсутствии параметров у f в последней строке, по которым компилятор мог бы произвести разрешение перегрузки. То, что параметрами for_each передаются итераторы вполне определенного типа – для компилятора не имеет значения.

Можно предложить несколько «лобовых» способов решения проблемы:

1) Использование static_cast<>() для принудительного приведения к указателю на функцию нужного типа.

Приводим к указателю на void f(int i):

std::for_each(begin(int_c), end(int_c), static_cast<void (*)(int)>(&f));

Приводим к указателю на void f(string i):

std::for_each(begin(string_c), end(string_c), static_cast<void (*)(string)>(&f));

2) Определение указателей на функции и их использование.

Используем void f(int i):

 void (*fp_int)(int) = &f;
  for_each(begin(int_c), end(int_c), fp_int);

Используем void f(string i):

void (*fp_string)(string) = &f;
for_each(begin(string_c), end(string_c), fp_string); 

3) Создание явных специализаций для шаблонной функции for_each:

Используем void f(int i):

std::for_each<vector<int>::const_iterator, void(*)(int) >(int_c.begin(), int_c.end(), f);

Используем void f(string i):

std::for_each<vector<string>::const_iterator, void(*)(string)  >(string_c.begin(), string_c.end(), f);

или более компактно:

std::for_each<decltype(int_c.begin()), void(*)(int) >(int_c.begin(), int_c.end(), f);
std::for_each<decltype(string_c.begin()), void(*)(string)  >(string_c.begin(), string_c.end(), f);

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

Необходимо также учитывать возможности:

  • существования неопределенного количества параметров у функции f,
  • наличия возвращаемого значения функции,
  • передачи параметров по ссылке,
  • существования cv-квалификаторы параметров,
  • возникновения исключений в функции f.

Поэтапно разберем построение такой оболочки. Лямбда-функция – хорошая основа. При поддержке компилятором стандарта С++11 это можно реализовать так:

for_each(begin(int_c), end(int_c), [](int a) { return f(a); });
for_each(begin(string_c), end(string_c), [](string a) { return f(a); });

или с использованием decltype():

for_each(begin(int_c), end(int_c), [](decltype(*int_c.begin()) a) { return f(a); });
for_each(begin(string_c), end(string_c), [](decltype(*string_c.begin()) a) { return f(a); });

При поддержке компилятором стандарта С++14 имеем для полиморфной лямбды следующее решение:

auto l = [](auto a) { return f(a); };

for_each(begin(int_c), end(int_c), l);
for_each(begin(string_c), end(string_c), l);

Итак, мы передаем параметром в лямбду параметр функции f, и потом вызываем f с этим параметром. Но тут есть проблема: мы не знаем заранее – являются параметры функции f значениями или ссылками? Поэтому необходимо использовать идеальную передачу.

auto l = [](auto&& a) { return f(std::forward<decltype(a)>(a)); };
for_each(begin(int_c), end(int_c), l);
for_each(begin(string_c), end(string_c), l);

По теме идеальной передачи (perfect forwarding) можно прочитать у Мейерса [1]. Более понятный (для меня во всяком случае) вариант изложения того же материала можно найти в статье[2], перевод которой есть на Хабре [3].

Для неопределенного числа параметров функции указываем вариативными параметры лямбды и вызываем std::forward для каждого передаваемого в f параметра.

 auto l = [](auto&&... a) { return f(std::forward<decltype(a)>(a)...); };

Добавляем спецификатор времени компиляции noexcept[4] и оператор noexcept[5] для указания компилятору – будет ли функция f выбрасывать исключения[6].

  auto l = [](auto&&... a) \
        noexcept(noexcept(f(std::forward<decltype(a)>(a)...)))\
    { return f(std::forward<decltype(a)>(a)...); };

Добавляем вывод типа возвращаемого значения для конструируемой лямбды.

   auto l = [](auto&&... a) \
        noexcept(noexcept(f(std::forward<decltype(a)>(a)...)))\
        -> decltype(f(std::forward<decltype(a)>(a)...))\
    { return f(std::forward<decltype(a)>(a)...); };

Если раздражает трехкратное повторение кода и не гнушаемся использовать макросы – можно уменьшить размер на вызывающей стороне.

#define LIFT(foo) \
[](auto&&... x) \
noexcept(noexcept(foo(std::forward<decltype(x)>(x)...))) \
-> decltype(foo(std::forward<decltype(x)>(x)...)) \
{ return foo(std::forward<decltype(x)>(x)...); }

В итоге имеем оболочку, которая помогает компилятору разрешить перегружаемую функцию.

for_each(begin(int_c), end(int_c), LIFT(f));
for_each(begin(string_c), end(string_c), LIFT(f));

[1] S.Meyers “Effective modern C++” Item 24: Distinguish universal references from rvalue references.
[2] eli.thegreenplace.net/2014/perfect-forwarding-and-universal-references-in-c
[3] habr.com/ru/post/242639
[4] en.cppreference.com/w/cpp/language/noexcept_spec
[5] en.cppreference.com/w/cpp/language/noexcept
[6] habr.com/ru/post/164221
[7] www.fluentcpp.com/2017/08/01/overloaded-functions-stl
[8] www.youtube.com/watch?v=I3T4lePH-yA
Tags:
Hubs:
+15
Comments8

Articles

Change theme settings