Предполагая, что я хочу реализовать функции с асинхронным поведением или просто хочу использовать указатели на функции в любом случае, вызывает ли вызов указателя функции то, что предоставляется, чтобы вызвать вызов связанной функции сразу после следующей инструкции?
пример
#include <iostream>
#include <cstdint>
int triple(int a) { return a * 3; }
void foo() { std::cout << "executing foo()" << '\n'; }
using fptrT = int (*)(int);
int main()
{
fptrT p = triple;
p(3);
foo();
}
Что оба стандарта говорят о том, что происходит, когда выражение p(3)
оценивается и когда будет выполняться foo()
?
Ответ Кристопа верен. Это дополнение.
Указатель функции сам по себе не может обеспечить асинхронное поведение. Стандарты фактически запрещают это. Я гораздо больше знаком с стандартом C, чем C++, поэтому я буду использовать его. Я понимаю, что оба должны быть примерно одинаковыми в этом вопросе.
Начнем с определения вызова функции в C, указанного в пункте 6.5.2.2 пункта 3:
Постфиксное выражение, за которым следуют скобки(), содержащие возможно пустой список выражений comma-, является вызовом функции. Постфиксное выражение обозначает вызываемую функцию. Список выражений указывает аргументы функции.
И изменено ограничение в пункте 1:
Выражение, которое обозначает вызываемую функцию (92), должно иметь указатель на функцию, возвращающую функцию void, или возвращающий полный тип объекта, отличный от типа массива.
Важно отметить, что в сопроводительной сноске 92 говорится:
Чаще всего это результат преобразования идентификатора, который является обозначением функции.
Таким образом, стандарт C11 в основном определяет вызов функции как то, что вызывает указатель на функцию. И для этой цели именованные функциональные идентификаторы автоматически преобразуются в указатели функций на код в идентификаторе. Таким образом, C не видит разницы между функциями и указателями функций.
Хотя всегда полезно ссылаться на стандарт, также очень полезно просто взглянуть на то, как твердые реализации делают что-то. Давайте проведем тест, где мы напишем довольно простой код, а затем посмотрим на основную сборку
Код:
#include <stdio.h>
#include <stdlib.h>
typedef void (*my_func_ptr)(int,int);
void my_function(int x, int y)
{
printf("x = %d, y = %d, x + y = %d\n",x,y,x+y);
}
int main()
{
/* declared volatile so the compiler has to call the function through
* the pointer and cannot optimize it to call the function directly */
volatile my_func_ptr fp = my_function;
my_function(3,5);
fp(3,6);
return 0;
}
Я скомпилировал код с помощью gcc
в Mac OS X с оптимизацией по умолчанию (gcc -o fptr fptr.c
), который на самом деле является gcc-интерфейсом библиотеки LLVM. Чтобы посмотреть на сборку, я запустил программу под lldb
, установил lldb
останова на main
и выпустил disassemble -f
, которая разбирает текущую функцию. Я использую settings set target.x86-disassembly-flavor intel
для сборки в стиле Intel. По умолчанию в lldb
используется lldb
AT & T, который выглядит несколько иначе.
main
процедура сборки заключается в следующем:
push rbp
mov rbp, rsp
sub rsp, 0x20 ; sets up the stack frame
mov edi, 0x3 ; my_function(3,5). 1st arg: edi
mov esi, 0x5 ; 2nd arg: esi
lea rax, qword ptr [rip - 0x59] ; loads address of my_function into rax
mov dword ptr [rbp - 0x4], 0x0
mov qword ptr [rbp - 0x10], rax ; saves address of my_function on stack
call 0x100000ed0 ; explicit call to my_function
mov eax, 0x0
mov edi, 0x3 ; fp(3,6). 1st arg: edi
mov esi, 0x6 ; 2nd arg: esi
mov rcx, qword ptr [rbp - 0x10] ; rcx <- address of my_function from stack
mov dword ptr [rbp - 0x14], eax
call rcx ; call address at rcx
mov eax, dword ptr [rbp - 0x14]
add rsp, 0x20
pop rbp
ret
Обратите внимание, что оба вызова функций по существу одинаковы. Они используют одну и ту же сборку. Оба раза фактический вызов вызывается с помощью call
op. Единственное различие заключается в том, что в первый раз адрес жестко закодирован, а во второй раз адрес сохраняется в регистре rcx
. Также обратите внимание, что нет ничего асинхронного в отношении кода.
Когда вы начинаете рассуждать о точках последовательности, вы фактически обнаруживаете, что в пределах одного потока стандарт запрещает такое асинхронное поведение, которое вы ожидали. В большинстве случаев C11 ограничивает компилятор для выполнения кода, который разделяется точкой последовательности в последовательном порядке. В разделе 5.1.2.3 (выполнение программы) порядок выполнения программы определяется как последовательность точек последовательности. Соответствующее определение, по существу, содержится в пункте 3:
Последовательность перед этим представляет собой асимметричное, транзитивное, парное отношение между оценками, выполняемыми одним потоком, что вызывает частичный порядок среди этих оценок. При любых двух оценках A и B, если A секвенирован до B, то выполнение A должно предшествовать исполнению B.
И позже в этом пункте:
Наличие точки последовательности между оценкой выражений A и B означает, что каждое вычисление значения и побочный эффект, связанные с A, секвенируются перед вычислением каждого значения и побочным эффектом, связанным с B.
В принципе, это устанавливает, что код, разделенный точкой последовательности, должен выполняться синхронно (в порядке). Тем не менее, стандарт предоставляет исключение, если компилятор может обосновать, что два фрагмента кода не могут влиять друг на друга, в пункте 4:
В абстрактной машине все выражения оцениваются в соответствии с семантикой. Фактическая реализация не должна оценивать часть выражения, если она может вывести, что ее значение не используется и что не требуются побочные эффекты (в том числе любые вызванные вызовом функции или доступом к изменчивому объекту).
Итак, как вводятся указатели на объекты? В Приложении C разъясняется, что точка последовательности находится между выражениями выражения, которые по существу являются утверждениями, заканчивающимися точкой с запятой (см. 6.8.3). Сюда входят вызовы функций.
Рассмотрим два последовательных вызова функций:
f();
g();
Ни один аргумент не принимает, поэтому рассуждение немного проще. Вызовы должны выполняться по порядку, если компилятор не может предположить, что какой-либо побочный эффект f()
не используется в g()
и наоборот. Единственный способ, с помощью которого компиляторы могут рассуждать об этом, заключается в том, что код функции доступен компилятору. В общем, это невозможно для указателей функций, поскольку указатель может указывать на любую функцию, которая удовлетворяет ограничениям типа указателя функции.
Обратите внимание, что в некоторых случаях компилятор может вывести правильную функцию (если указатель функции назначен только один раз и существует в локальной области), но это часто бывает не так. Таким образом, компилятор должен выполнить функции в представленном порядке, а первая функция должна вернуться до второго.
Стандарт C11 имеет разные правила для нарезки резьбы. Обратите внимание, что раздел 5.1.2.3 ограничивается выполнением в рамках одного потока. Библиотеки Coroutine, которые играют со стеклом, существенно разбивают модель машины C11 и привязаны к определенному набору реализаций (т.е. Не обязательно переносятся в любую среду C). Библиотека сопрограммы по существу должна предоставить собственный набор последовательных гарантий -o.
Указатель функции - это еще один способ вызова функции, используя сохраненный адрес вместо фиксированного заданного имени функции:
C++ 11, раздел. 5.2.2.1. Вызов функции - это постфиксное выражение, за которым следуют скобки, содержащие (...) аргументы функции. Для обычного вызова функции постфиксное выражение должно быть либо значением l, которое относится к функции, либо должно иметь указатель на тип функции.
Для C формулировка немного отличается (C11, раздел 6.5.2.2), но принципы одинаковы (за исключением указателя на функции-члены, которых нет в C).
Указатели функций могут использоваться для метакатегорий обратного вызова или для реализации шаблонов проектирования, таких как шаблон стратегии, для динамического изменения универсального алгоритма. В C++ теперь есть более мощные альтернативы, такие как лямбда-функции или функциональные объекты.
Если вы ищете вызов асинхронной функции, вы должны посмотреть на std::async()
:
std::future<int> ft = std::async (triple,3); // invoque asynchronously
//... remaining code
bool myresult = ft.get(); // get result when it is needed
Обратите внимание на ваш комментарий: сложно прогнозировать производительность, так как это зависит от реализации библиотеки, ОС и работоспособности. Но для меня это оказалось довольно эффективным: например, в MSVC2013, недавние эксперименты показали мне, что потоки, созданные для async, повторно используются, когда это возможно, тем самым уменьшая накладные расходы на создание до минимума. Использование многоядерного оборудования async действительно позволяет увеличить общую пропускную способность.
async
, тоже странные, у них есть dtor, который блокирует.
async
работать? Может быть, вам следует немного уточнить ваши ожидания. Деструктор блокируется, потому что асинхронный объект должен быть очищен, когда он больше не нужен. Это в целом верно для любого объекта C ++.