Skip to content

Latest commit

 

History

History
276 lines (209 loc) · 11.6 KB

function_pass_and_address_restriction.md

File metadata and controls

276 lines (209 loc) · 11.6 KB

Как передать стандартную функцию и ничего не сломать

Предположим, вам нужно заниматься какими-то вычислениями по долгу службы (или вы просто студент и вам кровь из носу надо выполнить задание по численным методом).

И вы взяли готовую прекрасную функцию интегрирования:

template <class F>
concept NumericFunction = std::is_invocable_v<F, float> && requires (float arg, F f) {
    { f(arg) } -> std::convertible_to<float>;
};

float integrate(NumericFunction auto f)  {
    float sum = 0;
    /*Мы не будем вдаваться в подробности точности, сходимости, и шагов разбиения и выбора точки, хотя это тоже очень важно, но в другой книжке*/
    for (float x : std::views::iota(1, 26)) {
        sum += f(x);
    }
    return sum;
}

Отлично. Вы начинаете ее тестировать на какой-нибудь стандартной функции

#include <cmath>
...
int main() {
    return integrate(sqrt); // все ок? (приведение к int считаем нормальным)
}

Вроде да.

На самом деле нет. И проблем тут как минимум две

  1. Стандартная библиотека C++ содержит в себе стандартную библиотеку C, что делает использование стандартных математических функций особенно болезненным:
static_assert(std::abs(5.8) > 5.5);
static_assert(abs(5.8) > 5.5);

//---------------------

<source>:26:24: error: static assertion failed
   26 | static_assert(abs(5.8) > 5.5);
      |               ~~~~~~~~~^~~~~
<source>:26:24: note: the comparison reduces to '(5.0e+0 > 5.5e+0)'
Compiler returned: 1

Окей. Мы поняли. Надо использовать std::sqrt, чтоб не попасть не в ту перегрузку

int main() {
    return integrate(std::sqrt);
}

// ---------------

<source>:22:21: error: no matching function for call to 'integrate(<unresolved overloaded function type>)'
   22 |     return integrate(std::sqrt);

Ага. overloaded function type. И как же нам тогда выбрать нужный?

Вы вышли в гугл с этим вопросом и первая ссылка привела вас на какой-нибудь Qt-форум (о, в Qt это распространенная проблема -- указать перегрузку при соединении сигналов и слотов), и самый релевантный ответ был: соорудить явное приведение типов указателей на функцию

int main() {
    return integrate(static_cast<float(*)(float)>(&std::sqrt));
}

Ура работает!;

Поздравляю...

  1. ...Вы нарушили пункт 16.4.5.2.6 и ваша программа будет оштрафована

Let F denote a standard library function ([global.functions]), a standard library static member function, or an instantiation of a standard library function template. Unless F is designated an addressable function, the behavior of a C++ program is unspecified (possibly ill-formed) if it explicitly or implicitly attempts to form a pointer to F.

Вызов integrate(static_cast<float(*)(float)>(&std::sqrt)); делает именно это. Вы взяли указатель на функцию. Указатели почти на любую функцию стандартной библиотеки брать нельзя.

Изначальный вариант с return integrate(sqrt), использующий sqrt из библиотеки C также попадает в эту ловушку, только неявно.

А с C++20 грозятся, что может перестать компилироваться, но я пока примера тому не видел.

Почему нельзя?

А кто вам сказал, что это функция?

Да, почти все функции стандартной библиотеки C++17, после инстанциирования шаблонов, все-таки оказываются нормальными функциями и потому у нас уж сколько лет все работает.

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

С C++20 (вдохновленные ranges Эрика Ниблера) новые (а также потенциально старые после перехода std на модули) функции внезапно могут оказаться ниблоидами. Глобальными объектами с определенным operator() -- так что они могут выглядеть и крякать как старые добрые функции, но таковыми не быть. И если вы использовали С-style каст вместо громоздкого static_cast, то вас могут ждать интересные результаты:

// https://godbolt.org/z/98Y6zv6nj
// старая версия
// float f(float a) {
//     return a;
// }

// вы обновились и теперь это ниблоид!
auto f = [](float a) -> float {
    return a;
};

int main() {
    return integrate((float(*)(float))(&f));
    // Segfault
}

Положение могло бы спасти отсутствие & перед именем функции (для функций и лямбд применяется неявный decay к указателю):

// https://godbolt.org/z/KnP8q7e3r
// float f(float a) {
//     return a;
// }

auto f = [](float a) -> float {
    return a;
};

int main() {
    return integrate((float(*)(float))(f));
    // комилируется и работает
}

// -----------------------------

// https://godbolt.org/z/fqzdse1Ya

// ниблоиды в std чаще определяются так, а не с помощью лямбл
struct {
    static float operator()(float x) {
        return x;
    } 
} f;

int main() {
    // не компилируется и нам ужасно повезло что это так!
    return integrate((float(*)(float))(f));
}

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

Что же делать?

Хорошая новость: если когда-нибудь вся замечательная гора стандартных функций станет вызываемыми объектами, ваш integrate(std::sqrt) будет компилироваться и работать правильно из коробки. И все будут счастливы.

Плохая новость: замечательно не будет, поэтому придется писать код

Проблема решится оборачиваением вызова к std функции в вашу функцию или в лямбду.

int main() {
    return integrate([](float x) {
        return std::sqrt(x);
    });
}

или даже можно завести вспомогательный макрос (с C++20 он выглядит чуть менее страшно чем обычно)

// https://godbolt.org/z/WPM9dx8Yx
#define LAMBDA_WRAP(f) []<class... T>(T&&... args) \
 noexcept(noexcept(f(std::forward<T>(args)...))) -> decltype(auto) \
 { return f(std::forward<T>(args)...); }

int main() {
    return integrate(LAMBDA_WRAP(std::sqrt));
}

Причем вариант с лямбдой, а не с функцией будет почти всегда предпочтительнее по соображениям оптимизаций. Смотрите:

Если вызов шаблонной функции integrate по какой-либо причине не может быть заинлайнен компилятором и вы передаете указатель на функцию, компилятор не имеет никакого выбора кроме как честно генерировать call по этому указателю:

// https://godbolt.org/z/7j68n6njq
#define LAMBDA_WRAP(f) []<class... T>(T&&... args) noexcept(noexcept(f(std::forward<T>(args)...))) -> decltype(auto) { return f(std::forward<T>(args)...); }

float my_sqrt(float f) {
    return std::sqrt(f);
}

int main() {
    return integrate(my_sqrt) + integrate(LAMBDA_WRAP(std::sqrt));
}

/*
// C функцией
float integrate<float (*)(float)>(float (*)(float)):
        push    rbp
        mov     rbp, rdi
        ...
.L24:
        pxor    xmm0, xmm0
        cvtsi2ss        xmm0, ebx
        add     ebx, 1
        call    rbp  // ! нет информации о функии -- вызов по указателю
        addss   xmm0, DWORD PTR [rsp+12]
        movss   DWORD PTR [rsp+12], xmm0
        cmp     ebx, 26
        ...
        ret
*/
// C лямбдой
/*
float integrate<main::{lambda<typename... $T0>(($T0&&)...)#1}>(main::{lambda<typename... $T0>(($T0&&)...)#1}) [clone .isra.0]:
        ...
.L16:
        pxor    xmm0, xmm0
        cvtsi2ss        xmm0, ebx
        ucomiss xmm2, xmm0
        ja      .L21
        sqrtss  xmm0, xmm0 // ! sqrt подставлен
        add     ebx, 1
        addss   xmm1, xmm0
        cmp     ebx, 26
        jne     .L16
.L11:
        ...
.L21:
        movss   DWORD PTR [rsp+12], xmm1
        add     ebx, 1
        call    sqrtf  /// ! sqrt подставлен
        ...
*/

Почему лямбда почти всегда предпочтительнее, но не всегда?

GCC и Clang, например, генерирует копию кода для каждого вызова с лямбдой, даже если они одинаковые. Ну просто потому что так необходимо: у каждой лямбды должен быть уникальный тип.

// https://godbolt.org/z/8jazdx5n7
int main() {
    return integrate(my_sqrt) +
           integrate(LAMBDA_WRAP(std::sqrt)) + 
           integrate(LAMBDA_WRAP(std::sqrt)) +
           integrate(LAMBDA_WRAP(std::sqrt));
}

Что поледать, раздутие кода -- известный результат мономорфизации шаблонов/generic функций.

Переиспользуйте лямбду, и будет лучше:

// https://godbolt.org/z/h14ceaaYc
// сгенерированный код в 2 раза меньше чем для примера выще
int main() {
    auto sqrt_f = LAMBDA_WRAP(std::sqrt);
    return integrate(my_sqrt) +
           integrate(sqrt_f) + 
           integrate(sqrt_f) +
           integrate(sqrt_f);
}

Полезные ссылки

  1. https://en.cppreference.com/w/cpp/language/extending_std#Designated_addressable_functions
  2. Rust Functions Are Weird (But Be Glad) - https://www.youtube.com/watch?v=SqT5YglW3qU