Указатели на функции возвращаются сразу в C и C ++?

0

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

пример

#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()?

  • 8
    Вызов функции через указатель ничем не отличается от вызова функции напрямую.
  • 0
    @MarkRansom это звучало в моей голове по некоторым причинам ... Мне кажется странным, что оба варианта не отличаются, я не могу это объяснить. В любом случае, это нельзя использовать для реализации функций с асинхронным поведением? Как мне идти отсюда?
Показать ещё 16 комментариев
Теги:
pointers
asynchronous
function-pointers

2 ответа

7
Лучший ответ

Ответ Кристопа верен. Это дополнение.

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

Что говорит стандарт C11 о функциях и указателях функций

Начнем с определения вызова функции в 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 говорит о точках последовательности

Когда вы начинаете рассуждать о точках последовательности, вы фактически обнаруживаете, что в пределах одного потока стандарт запрещает такое асинхронное поведение, которое вы ожидали. В большинстве случаев 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.

  • 0
    Я знаю, что я был неправ, но вы тоже «обманываете», потому что вы ссылаетесь на детали реализации, более того, вы ссылаетесь на одну реализацию на конкретной машине. Но я понял, я просто видел это иначе, чем на самом деле.
  • 0
    Я не изменяю. Кто-то другой может ссылаться на стандарт, мне сейчас это безразлично, тем более что вы, кажется, больше озабочены C ++, чем C, и я стараюсь избегать стандарта C ++. Но я добавлю кое-что из стандарта C на точки последовательности, если нужно.
Показать ещё 2 комментария
6

Указатель функции - это еще один способ вызова функции, используя сохраненный адрес вместо фиксированного заданного имени функции:

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 действительно позволяет увеличить общую пропускную способность.

  • 0
    Ужасно, не ваш пример, а стандартная реализация, она основана на потоках, и фьючерсы, возвращаемые через async , тоже странные, у них есть dtor, который блокирует.
  • 0
    Как еще вы ожидаете, что async работать? Может быть, вам следует немного уточнить ваши ожидания. Деструктор блокируется, потому что асинхронный объект должен быть очищен, когда он больше не нужен. Это в целом верно для любого объекта C ++.
Показать ещё 7 комментариев

Ещё вопросы

Сообщество Overcoder
Наверх
Меню