Когда использовать const void *?

У меня есть эта очень простая тестовая функция, которую я использую, чтобы выяснить, что происходит с квалификатором const.

int test(const int* dummy)
{
   *dummy = 1;
   return 0;
}

Это вызывает ошибку GCC 4.8.3. Но это компилируется:

int test(const int* dummy)
{
   *(char*)dummy = 1;
   return 0;
}

Итак, похоже, что спецификатор const работает только в том случае, если я использую аргумент без кастования другому типу.

Недавно я видел коды, которые использовали

test(const void* vpointer, ...)

По крайней мере, для меня, когда я использовал void *, я склонен использовать его для char * для арифметики указателя в стеках или для трассировки. Как const void * запретить функции подпрограмм изменять данные, на которые указывает vpointer?

Ответ 1

const int *var;

const - контракт. Получив параметр const int *, вы "сообщите" вызывающему, что вы (вызываемая функция) не будете изменять объекты, на которые указывает указатель.

В вашем втором примере явно разрывает этот контракт, отбрасывая квалификатор const и затем изменяя объект, на который указывает полученный указатель. Никогда не делайте этого.

Этот "контракт" применяется компилятором. *dummy = 1 не будет компилироваться. Бросок - это способ обойти это, сообщая компилятору, что вы действительно знаете, что делаете, и позволяете вам это делать. К сожалению, "я действительно знаю, что делаю", обычно не так.

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


Undefined Поведение:

Обратите внимание, что в то время как сам листинг является технически законным, изменение значения, объявленного как const, - это Undefined Поведение. Так что технически исходная функция в порядке, если переданный ей указатель указывает на данные, объявленные изменчивыми. Иначе это Undefined Поведение.

подробнее об этом в конце сообщения


Что касается мотивации и использования, давайте возьмем аргументы функций strcpy и memcpy:

char* strcpy( char* dest, const char* src );
void* memcpy( void* dest, const void* src, std::size_t count );

strcpy работает с строками char, memcpy работает с общими данными. Хотя я использую пример strcpy, следующее обсуждение для обоих одинаково, но с char * и const char * для strcpy и void * и const void * для memcpy:

dest есть char *, потому что в буфере dest функция поместит копию. Функция будет изменять содержимое этого буфера, поэтому оно не является константой.

src const char *, потому что функция только считывает содержимое буфера src. Он не меняет его.

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


const и void являются ортогональными. Это все обсуждение выше о const применяется к любому типу (int, char, void,...)

void * используется в C для "общих" данных.


Еще больше на Undefined Поведение:

Случай 1:

int a = 24;
const int *cp_a = &a; // mutabale to const is perfectly legal. This is in effect
                      // a constant view (reference) into a mutable object

*(int *)cp_a = 10;    // Legal, because the object referenced (a)
                      // is declared as mutable

Случай 2:

const int cb = 42;
const int *cp_cb = &cb;
*(int *)cp_cb = 10;    // Undefined Behavior.
                       // the write into a const object (cb here) is illegal.

Я начал с этих примеров, потому что их легче понять. Отсюда есть только один шаг для использования аргументов:

void foo(const int *cp) {
    *(int *)cp = 10;      // Legal in case 1. Undefined Behavior in case 2
}

Случай 1:

int a = 0;
foo(&a);     // the write inside foo is legal

Случай 2:

int const b = 0;
foo(&b);     // the write inside foo causes Undefined Behavior

Снова я должен подчеркнуть: если вы действительно не знаете, что делаете, и все люди, работающие в настоящем и будущем в коде, являются экспертами и понимают это, и у вас есть хорошая мотивация, если все вышеперечисленные встретил, никогда не отбрасывал константу!!

Ответ 2

int test(const int* dummy)
{
   *(char*)dummy = 1;
   return 0;
}

Нет, это не сработает. Устранение константы (с достоверными данными const) - это поведение undefined, и ваша программа, скорее всего, потерпит крах, если, например, реализация поместит данные const в ПЗУ. Тот факт, что "он работает" не меняет того факта, что ваш код плохо сформирован.

По крайней мере, для меня, когда я использовал void *, я склоняю его к char * для арифметика указателя в стеках или для трассировки. Как может const void * предотвращать функции подпрограммы от изменения данных, при которых vpointer указывает?

A const void* означает указатель на некоторые данные, которые нельзя изменить. Чтобы его прочитать, да, вы должны использовать его для конкретных типов, таких как char. Но я сказал, что читаю, а не пишу, что опять же является UB.

Здесь подробно описано . C позволяет полностью обойти безопасность типа: это ваша задача, чтобы предотвратить это.

Ответ 3

Возможно, что данный компилятор на данной ОС может помещать некоторые из своих данных const в страницы памяти только для чтения. Если это так, попытка записи в это место не удалась бы на аппаратном обеспечении, например, вызвала бы общую ошибку защиты.

Квалификатор const просто означает, что запись имеет undefined поведение. Это означает, что языковой стандарт позволяет программе сбой, если вы делаете (или что-то еще). Несмотря на это, C позволяет вам стрелять в ногу, если вы думаете, что знаете, что делаете.

Вы не можете остановить подпрограмму от переинтерпретации битов, которые вы ей даете, но она хочет и запускает любую машинную инструкцию по их желанию. Функция библиотеки, которую вы вызываете, может даже быть написана на ассемблере. Но выполнение этого указателя const - это undefined поведение, и вы действительно не хотите вызывать поведение undefined.

Сверху моей головы, один редкий пример, где это может иметь смысл: предположим, что у вас есть библиотека, которая передает параметры дескриптора. Как он генерирует и использует их? Внутри они могут быть указателями на структуры данных. Таким образом, это приложение, в котором вы могли бы typedef const void* my_handle;, чтобы компилятор выдавал ошибку, если ваши клиенты пытаются разыграть ее или сделать арифметику по ошибке, а затем вернуть ее к указателю на вашу структуру данных внутри ваших библиотечных функций. Это не самая безопасная реализация, и вы хотите быть осторожным в отношении злоумышленников, которые могут передавать произвольные значения в вашу библиотеку, но очень низко накладные.