Снова указатели на функции C ++.Путаница в отношении синтаксиса - PullRequest
3 голосов
/ 06 мая 2019

На на этой странице Я нашел хороший пример указателей на функции в C ++ (а также функторов, но этот вопрос не о функторах).Ниже приведены некоторые копии этой страницы.

#include <iostream>

double add(double left, double right) {
  return left + right;
}

double multiply(double left, double right) {
  return left * right;
}

double binary_op(double left, double right, double (*f)(double, double)) {
  return (*f)(left, right);
}

int main( ) {
  double a = 5.0;
  double b = 10.0;

  std::cout << "Add: " << binary_op(a, b, add) << std::endl;
  std::cout << "Multiply: " << binary_op(a, b, multiply) << std::endl;

  return 0;
}

Я понимаю код в общих чертах, но есть пара вещей, которые меня всегда смущают.Функция binary_op() принимает указатель на функцию *f, но когда она используется, например, в строке 19 binary_op(a, b, add), передается символ функции add, а не то, что можно считать указателем &add.Теперь вы можете сказать, что это потому, что символ add является указателем;это адрес бита кода, соответствующего функции add().Очень хорошо, но здесь все еще есть расхождение типов.Функция binary_op() принимает *f, что означает, что f является указателем на что-то.Я передаю add, который сам по себе является указателем на код.(Верно?) Итак, f присваивается значение add, что делает f указателем на код, что означает, что f является функцией, аналогичной add, что означает, что f долженназываться как f(left, right), точно так же, как add следует называть, но в строке 12 он называется как (*f)(left, right), что мне кажется неправильным, потому что это похоже на написание (*add)(left, right) и *addэто не функция, это первый символ кода, на который указывает add.(Верно?)

Я знаю, что замена оригинального определения binary_op() на следующее также работает.

double binary_op(double left, double right, double f(double, double)) {
  return f(left, right);
}

И на самом деле, это имеет для меня гораздо больше смысла, нооригинальный синтаксис не имеет смысла, как я объяснил выше.

Итак, почему синтаксически правильно использовать (*f) вместо f?Если символ func сам по себе является указателем, то что конкретно означает фраза «указатель на функцию» или «указатель на функцию»?Исходный код в настоящее время означает, что когда мы пишем double (*f)(double, double), что тогда будет f?Указатель на указатель (потому что (*f) сам по себе является указателем на бит кода)?Является ли символ add такой же вещью, как (*f), или такой же вещью, как f?

Теперь, если ответ на все это будет "Да, синтаксис C ++странно, просто запомните синтаксис указателя на функцию и не подвергайте его сомнению. ", тогда я неохотно приму это, но мне бы очень хотелось получить правильное объяснение того, что я здесь не так.

Я прочитал этот вопрос , и я думаю, что понимаю это, но не нашел его полезным в решении моей путаницы.Я также прочитал этот вопрос , который также не помог, потому что он напрямую не решает мою проблему несоответствия типов.Я мог бы продолжать читать море информации в Интернете, чтобы найти мой ответ, но эй, это то, что Stack Overflow для права?

Ответы [ 3 ]

3 голосов
/ 06 мая 2019

Это потому, что указатель на функцию C особенный.

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

Тогда странные вещи это там:

return (*f)(left, right);

Итак, почему синтаксически правильно использовать (*f) вместо f?

Оба действительны, вы можете переписать код следующим образом:

return f(left, right);

Это связано с тем, что оператор разыменования возвращает ссылку на функцию, и обе ссылки на функцию или указатель на функцию считаются вызываемыми.

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

return (*******f)(left, right); // ah! still works
1 голос
/ 06 мая 2019

Прежде всего параметр функции, указанный в объявлении функции, корректируется для указания на функцию, когда компилятор определяет тип параметра.Так, например, следующие объявления функций

void f( void h() );
void f( void ( *h )() );

эквивалентны и объявляют одну и ту же функцию.

Рассмотрим следующую демонстрационную программу

#include <iostream>

void f( void h() );
void f( void ( *h )() );

void h() { std::cout << "Hello Ray\n"; }

void f( void h() ) { h(); }

int main()
{
    f( h );
}

Из стандарта c ++ 17 (11.3.5 Функции):

5 Тип функции определяется с использованием следующих правил.Тип каждого параметра (включая пакеты параметров функции) определяется из его собственных decl-specier-seq и декларатора.После определения типа каждого параметра любой параметр типа «массив из T» или типа функции T настраивается на «указатель на T ».

С другой стороныстороны, в соответствии со стандартом C ++ 17

9 Когда для данного аргумента нет параметра, аргумент передается таким образом, что принимающая функция может получить значение аргумента, вызвав va_arg(21,11).[Примечание: этот абзац не применяется к аргументам, передаваемым в пакет параметров функции.Пакеты параметров функции расширяются во время создания шаблона (17.6.3), поэтому каждый такой аргумент имеет соответствующий параметр, когда фактически вызывается специализация шаблона функции.- примечание к концу] Стандартные преобразования lvalue-to-rvalue (7.1), array-to-pointer (7.2) и function-to-pointer (7.3) выполняются для выражения аргумента

Так в чем же разница между этими двумя объявлениями

void f( void h() );
void f( void ( *h )() );

Для первого объявления вы можете рассматривать параметр h в теле функции как typedef для указателя функции.

typedef void ( *H )();

Например,

#include <iostream>

void f( void h() );
void f( void ( *h )() );

void h() { std::cout << "Hello Ray\n"; }


typedef void ( *H )();

void f( H h ) { h(); }

int main()
{
    f( h );
}

Согласно стандарту C ++ 17 (8.5.1.2 вызов функции)

1 Вызов функции - это выражение postfix, за которым следуют скобкисодержащий, возможно, пустой, разделенный запятыми список предложений-инициализаторов, которые составляют аргументы функции.Постфиксное выражение должно иметь тип функции или тип указателя функции .

Так что вы также можете определить функцию как

void f( void h() ) { ( *h )(); }

или даже как

void f( void h() ) { ( ******h )(); }

, потому что когда оператор * применяется к имени функции, тогда имя функции неявно преобразуется в pijnter для функции.

1 голос
/ 06 мая 2019

В исходном коде в настоящее время, когда мы пишем double (*f)(double, double), что тогда будет f?

Тип f равен double (*)(double, double), т.е. это указатель на функцию типа double(double,double).

потому что (*f) сам по себе является указателем

Это не так.

В: Что вы получаете, когда косвенно обращаетесь через указатель (например, в *f)? A: Вы получаете ссылку на lvalue. Например, с учетом указателя на объект int* ptr тип выражения *ptr равен int&, т.е. имеет значение lvalue для int.

То же самое относится и к указателям на функции: когда вы косвенно обращаетесь к указателю на функцию, вы получаете ссылку lvalue на указанную функцию. В случае *f типом является double (&)(double, double), то есть ссылка на функцию типа double(double,double).

Является ли символ add такой же вещью, как (*f), или такой же вещью, как f?

Безусловное выражение id add - это то же самое, что и *f, т.е. это lvalue:

Стандартный черновик [expr.prim.id.unqual]

... Выражение является lvalue, если сущность является функцией ...


передается символ функции add, а не то, о чем можно подумать, как указатель &add. Теперь вы можете сказать, что это потому, что символ add является указателем;

Нет. Это не причина.

add не является указателем. Это lvalue. Но значения типа функции неявно преобразуются в указатель (это называется затуханием):

Стандартный проект [conv.func]

Значение l типа функции T может быть преобразовано в значение типа «указатель на T». Результатом является указатель на функцию.

Таким образом, семантически эквивалентно следующее:

binary_op(a, b,  add); // implicit function-to-pointer conversion
binary_op(a, b, &add); // explicit use of addressof operator

Итак, почему синтаксически правильно использовать (*f) вместо просто f?

Оказывается, что вызов функции lvalue имеет тот же синтаксис, что и вызов указателя функции:

Стандартный проект [expr.call]

Вызов функции - это постфиксное выражение, за которым следуют круглые скобки, содержащие возможно пустой, разделенный запятыми список предложений инициализатора, которые составляют аргументы функции. Постфиксное выражение должно иметь тип функции или тип указателя на функцию. Для вызова функции, не являющейся членом или статической функции-члена, выражение postfix должно быть либо lvalue, которое относится к функции (в этом случае стандартное преобразование функции в указатель ([conv .func]) подавляется в выражении postfix), или имеет указатель на функцию типа .

Это все одинаковые вызовы функций:

add(parameter_list);    // lvalue
(*f)(parameter_list);   // lvalue

(&add)(parameter_list); // pointer
f(parameter_list);      // pointer

P.S. Эти две декларации эквивалентны:

double binary_op(double, double, double (*)(double, double))
double binary_op(double, double, double    (double, double))

Это связано со следующим правилом, которое дополняет неявное затухание в указателе функции:

Стандартный черновик [dcl.fct]

Тип функции определяется с использованием следующих правил. Тип каждого параметра (включая пакеты параметров функции) определяется из его собственных decl-specier-seq и декларатора. После определения типа каждого параметра любой параметр типа «массив T» или типа функции T настраивается на «указатель на T» ...

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...