Назначение функции указателю функции, правильность аргумента const?

Сейчас я изучаю основы C++ и ООП в своем университете. Я не уверен на 100%, как работает указатель функции при назначении им функций. Я столкнулся со следующим кодом:

void mystery7(int a, const double b) { cout << "mystery7" << endl; }
const int mystery8(int a, double b) { cout << "mystery8" << endl; }

int main() {
    void(*p1)(int, double) = mystery7;            /* No error! */
    void(*p2)(int, const double) = mystery7;
    const int(*p3)(int, double) = mystery8;
    const int(*p4)(const int, double) = mystery8; /* No error! */
}

Насколько я понимаю, назначения p2 и p3 прекрасны, поскольку типы параметров функций совпадают, а константность верна. Но почему присваивания p1 и p4 не терпят неудачу? Разве не должно быть незаконным сопоставление const double/int с неконстантным double/int?


person edwardlam0328    schedule 09.06.2019    source источник
comment
Возможно, пример мог бы быть более интересным со ссылкой на const mon const. Поскольку мистерии 7 и 8 вызываются по значению, технически они всегда константны (точнее: неизменны).   -  person lalala    schedule 09.06.2019
comment
@lalala Это настоящая путаница терминов/идей. Неизменяемость и идентичность — несвязанные понятия.   -  person Lightness Races in Orbit    schedule 09.06.2019
comment
@lalala И если бы вы изменили пример так, как вы предлагаете, вопросов больше не было бы (потому что это поведение связано именно с передачей по значению)   -  person Lightness Races in Orbit    schedule 09.06.2019


Ответы (3)


Согласно стандарту C++ (C++ 17, 16.1 Перегружаемые объявления)

(3.4) — Объявления параметров, отличающиеся только наличием или отсутствием const и/или volatile, эквивалентны. То есть спецификаторы типа const и volatile для каждого типа параметра игнорируются при определении того, какая функция объявляется, определяется или вызывается.

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

void mystery7(int a, const double b);

и тип функции void( int, double ).

Также рассмотрите следующее объявление функции

void f( const int * const p );

Это эквивалентно следующему объявлению

void f( const int * p );

Именно второй const делает параметр постоянным (то есть объявляет сам указатель как постоянный объект, который нельзя переназначить внутри функции). Первая константа определяет тип указателя. Это не отбрасывается.

Обратите внимание на то, что хотя в стандарте C++ используется термин "константная ссылка", сами ссылки не могут быть константами, противоположными указателям. Это следующее объявление

int & const x = initializer;

это неверно.

Пока эта декларация

int * const x = initializer;

правильно и объявляет постоянный указатель.

person Vlad from Moscow    schedule 09.06.2019
comment
Спасибо за быстрый ответ. Это, безусловно, некоторые новые концепции, за которые я сейчас хватаюсь. Но что происходит, когда я пытаюсь передать (const int, double) в *p4? Не вызовет ли это, в свою очередь, ошибку компиляции, поскольку указатель указывает на функцию с параметрами (int, double)? - person edwardlam0328; 09.06.2019
comment
@ edwardlam0328 Нет, к ним относятся одинаково, согласно правилу, которое процитировал Влад. - person Lightness Races in Orbit; 09.06.2019
comment
@ edwardlam0328 Параметры — это локальные переменные функций, которые инициализируются с помощью аргументов с использованием инициализации копирования. Вы можете представить это следующим образом. недействительным f (инт х); константа int я = 10; е(я); void f( /*int x*/ ) { int x = i; /*...*/ } Это объявление int x = i; действителен, и константа может использоваться в качестве инициализатора. - person Vlad from Moscow; 09.06.2019
comment
Ваш цитируемый стандарт относится к поиску, а не к типам функций, не так ли? - person Lightness Races in Orbit; 09.06.2019
comment
Спасибо всем, что напомнили мне о передаче по значению! Я забыл это ключевое понятие. Итак, если я правильно понимаю, поскольку параметры передачи по значению создают локальную копию внутри тела функции, поэтому это не влияет на исходную переданную переменную типа const, поэтому таким образом мы как бы обходим ограничение const? И вот почему объявление указателя функции как (const int, double) или (int, double) не будет иметь значения, верно? - person edwardlam0328; 09.06.2019
comment
@ edwardlam0328 Более или менее, да. Разработчикам языка не нужно было должно делать это таким образом (и, в конце концов, const в значении по-прежнему имеет назначение и значение внутри тела функции!). Но они сделали это для того, чтобы оператору связи было все равно, я думаю. - person Lightness Races in Orbit; 09.06.2019
comment
const в значении параметра только имеет значение внутри тела функции, поэтому вопрос не столько в том, почему они совместимы, сколько в том, почему вообще разрешено уточнять параметры. Хотя технически то же самое можно сказать и об именах параметров, и они полезны для документации/удобочитаемости, поэтому есть аргумент, чтобы разрешить это. - person Leushenko; 10.06.2019

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

Хотя const для них повлияет на их использование внутри функции (для предотвращения несчастных случаев), в сигнатуре это в основном игнорируется. Это связано с тем, что constness объекта, переданного по значению, не оказывает никакого влияния на исходный скопированный объект в месте вызова.

Это то, что вы видите.

(Лично я думаю, что это дизайнерское решение было ошибкой; оно сбивает с толку и не нужно! Но это то, что есть. Обратите внимание, что оно взято из того же отрывка, который молча изменяет void foo(T arg[5]); на void foo(T* arg);, так что там уже много херни! с чем нам приходится иметь дело!)

Помните, однако, что это не просто стирает любое const в таком типе аргумента. В int* const указатель равен const, но в int const* (или const int*) указатель не const, а на const вещь. Только первый пример относится к constness самого указателя и будет удален.


[dcl.fct]/5 Тип функции определяется с использованием следующих правил. Тип каждого параметра (включая пакеты параметров функции) определяется его собственным decl-specifier-seq и декларатором. После определения типа каждого параметра любой параметр типа «массив T» или типа функции T корректируется как «указатель на T». После создания списка типов параметров все cv-qualifiers верхнего уровня, изменяющие тип параметра, удаляются при формировании типа функции. Результирующий список преобразованных типов параметров и наличие или отсутствие многоточия или пакета параметров функции является списком типов параметров функции. [ Примечание. Это преобразование не влияет на типы параметров. Например, int(*)(const int p, decltype(p)*) и int(*)(int, const int*) являются идентичными типами. — конец примечания ]

person Lightness Races in Orbit    schedule 09.06.2019
comment
IMO, это было хорошее решение - хочет ли реализация функции изменить скопированный объект или нет, это не то, что вызывающий должен знать. - person M.M; 10.06.2019
comment
@M.M Это в лучшем случае приятно, и это создает особый случай в системе типов, что приводит к путанице, такой как OP - person Lightness Races in Orbit; 10.06.2019

Бывает ситуация, когда добавление или удаление квалификатора const к аргументу функции является серьезной ошибкой. Это происходит, когда вы передаете аргумент по указателю.

Вот простой пример того, что может пойти не так. Этот код не работает в C:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// char * strncpy ( char * destination, const char * source, size_t num );

/* Undeclare the macro required by the C standard, to get a function name that
 * we can assign to a pointer:
 */
#undef strncpy

// The correct declaration:
char* (*const fp1)(char*, const char*, size_t) = strncpy;
// Changing const char* to char* will give a warning:
char* (*const fp2)(char*, char*, size_t) = strncpy;
// Adding a const qualifier is actually dangerous:
char* (*const fp3)(const char*, const char*, size_t) = strncpy;

const char* const unmodifiable = "hello, world!";

int main(void)
{
  // This is undefined behavior:
  fp3( unmodifiable, "Whoops!", sizeof(unmodifiable) );

  fputs( unmodifiable, stdout );
  return EXIT_SUCCESS;
}

Проблема здесь с fp3. Это указатель на функцию, которая принимает два аргумента const char*. Однако он указывает на вызов стандартной библиотеки strncpy()¹, первым аргументом которого является буфер, который он модифицирует. То есть fp3( dest, src, length ) имеет тип, который обещает не изменять данные, на которые указывает dest, но затем передает аргументы strncpy(), который изменяет эти данные! Это возможно только потому, что мы изменили сигнатуру типа функции.

Попытка изменить строковую константу является поведением undefined — мы фактически сказали программе вызывать strncpy( "hello, world!", "Whoops!", sizeof("hello, world!") ) — и на нескольких разных компиляторах, с которыми я тестировал, во время выполнения она не будет работать.

Любой современный компилятор C должен разрешать присваивание fp1, но предупреждать вас, что вы стреляете себе в ногу, используя либо fp2, либо fp3. В C++ строки fp2 и fp3 вообще не будут компилироваться без reinterpret_cast. Добавление явного приведения заставляет компилятор предположить, что вы знаете, что делаете, и отключает предупреждения, но программа по-прежнему не работает из-за своего неопределенного поведения.

const auto fp2 =
  reinterpret_cast<char*(*)(char*, char*, size_t)>(strncpy);
// Adding a const qualifier is actually dangerous:
const auto fp3 =
  reinterpret_cast<char*(*)(const char*, const char*, size_t)>(strncpy);

Этого не происходит с аргументами, передаваемыми по значению, потому что компилятор делает их копии. Пометка параметра, переданного значением const, просто означает, что функция не ожидает необходимости изменять свою временную копию. Например, если стандартная библиотека внутренне объявила char* strncpy( char* const dest, const char* const src, const size_t n ), она не сможет использовать идиому K&R *dest++ = *src++;. Это изменяет временные копии аргументов функции, которые мы объявили const. Поскольку это не влияет на остальную часть программы, C не будет возражать, если вы добавите или удалите квалификатор const, подобный этому, в прототипе функции или указателе функции. Обычно вы не делаете их частью общедоступного интерфейса в заголовочном файле, так как они являются деталями реализации.

¹ Хотя я использую strncpy() в качестве примера известной функции с правильной сигнатурой, в целом она устарела.

person Davislor    schedule 10.06.2019
comment
Обратите внимание, что использование strncpy(buf, src, sizeof(buf)); fputs(buf, stdout); в целом неверно, поскольку вывод strncpy не завершается нулем, если src не помещается в buf. - person M.M; 10.06.2019
comment
@M.M Я мог бы написать пример с memcpy(), но я думаю, что здесь он служит своей цели? - person Davislor; 10.06.2019
comment
@MM Добавил сноску. - person Davislor; 10.06.2019