Назначение функции указателю на функцию, правильность аргумента 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?
Ответы
Ответ 1
Согласно стандарту 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, который делает параметр постоянным (то есть он объявляет указатель как постоянный объект, который не может быть переназначен внутри функции). Первый const определяет тип указателя. Это не отбрасывается.
Обратите внимание на то, что хотя в стандарте C++ используется термин "константная ссылка", сами ссылки не могут быть постоянными в противоположность указателям. Это следующая декларация
int & const x = initializer;
это неверно.
Пока эта декларация
int * const x = initializer;
является правильным и объявляет постоянный указатель.
Ответ 2
Существует специальное правило для аргументов функций, передаваемых по значению.
Хотя const
на них будет влиять на их использование внутри функции (для предотвращения несчастных случаев), она в основном игнорируется в подписи. Это потому, что const
ность объекта, переданного по значению не имеет никакого влияния на оригинал скопирован из объекта в месте вызова.
Это то, что вы видите.
(Лично я думаю, что это дизайнерское решение было ошибкой; оно запутанное и ненужное! Но это то, чем оно является. Обратите внимание, что оно происходит из того же отрывка, который молча превращает void foo(T arg[5]);
в void foo(T* arg);
;, так что тут уже много хреновой фигни! t, с которой нам приходится иметь дело!)
Напомним, однако, что это не просто стирает const
в таком типе аргумента. В int* const
указатель является const
, но в int const*
(или const int*
) указатель является non- const
но относится к const
. Только первый пример относится к const
Несс самого указателя и будут удалены.
[dcl.fct]/5
Тип функции определяется с использованием следующих правил. Тип каждого параметра (включая пакеты параметров функции) определяется из его собственных decl-specier-seq и декларатора. После определения типа каждого параметра любой параметр типа "массив из T
" или типа функции T
настраивается на "указатель на T
". После создания списка типов параметров любые cv-квалификаторы верхнего уровня, модифицирующие тип параметра, удаляются при формировании типа функции. Результирующий список преобразованных типов параметров и наличие или отсутствие многоточия или пакета параметров функции представляет собой список параметров типа функции. [Примечание: это преобразование не влияет на типы параметров. Например, int(*)(const int p, decltype(p)*)
и int(*)(int, const int*)
являются идентичными типами. - конец примечания]
Ответ 3
Существует ситуация, когда добавление или удаление квалификатора 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()
, который модифицирует эти данные! Это возможно только потому, что мы изменили сигнатуру типа функции.
Попытка изменить строковую константу - это неопределенное поведение - мы фактически приказали программе вызвать 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
такой как в прототипе функции или указателе функции. Обычно вы не делаете их частью общедоступного интерфейса в заголовочном файле, так как это детали реализации.
St Хотя я использую strncpy()
в качестве примера хорошо известной функции с правильной подписью, в целом она устарела.