В модуле C (также известном как модуль компиляции) я хочу иметь некоторые личные данные, но предоставить их только для чтения внешнему миру. Я добиваюсь этого, имея поле в struct, объявленное в моем файле .c, и функцию, объявленную в моем файле .h, которая возвращает указатель на const в это поле. Например, для нить это может выглядеть следующим образом:
// header:
typdef struct foo foo;
const char *foostr(const foo *f);
// implementation:
struct foo
{
char *str;
};
const char *foostr(const foo *f)
{
return foo->str;
}
Теперь моя проблема в том, что у меня есть массив объектов, которые сами по себе являются массивами. Итак, в моем struct у меня есть указатель на массив, и из моей функции я пытаюсь вернуть указатель на соответствующий массив const. Рассмотрим следующий пример кода:
#include <stdlib.h>
#include <stdint.h>
typedef uint8_t shape[64];
typedef struct foo
{
shape *shapes;
} foo;
foo *createfoo(void)
{
foo *f = malloc(sizeof *f);
if (!f) return 0;
// 10 empty shapes:
f->shapes = calloc(10, sizeof *(f->shapes));
if (!f->shapes)
{
free(f);
return 0;
}
return f;
}
const shape *fooshapes(const foo *f)
{
return f->shapes;
}
Компилируя это с помощью gcc -c -std=c11 -Wall -Wextra -pedantic, я получаю следующее предупреждение:
constarr.c: In function ‘fooshapes’:
constarr.c:31:13: warning: pointers to arrays with different qualifiers are incompatible in ISO C [-Wpedantic]
return f->shapes;
~^~~~~~~~
Я понимаю, что двойной указатель несовместим с двойной указатель на const, а также причина этого, но я не думаю, что это связано, или это так? Итак, почему не разрешено неявно преобразовывать указатель на массив в указатель на массив const? И есть идеи, что мне делать вместо этого?
Что я сделал сейчас, так это добавление явного приведения типа этого:
const shape *fooshapes(const foo *f)
{
return (const shape *) f->shapes;
}
Это, конечно, заставляет компилятор замолчать, и я почти уверен, что на практике он всегда будет работать правильно. В этом случае «константная дыра» не может существовать, так как в случае с массивом нет неконстантного внутреннего указателя. Но у меня все еще остаются два вопроса:
const?@FBergo преобразование в неконстантное возможно, но все еще запрещено (неопределенные результаты). Я не пытаюсь защититься от неправильного использования C здесь, это, конечно, невозможно ..
Ну, clang не жалуется: godbolt.org/g/82j7rm ... Может быть, связанный: stackoverflow.com/a/8909208/4944425
@Bob__ спасибо, оба очень интересные :) Так что решение для меня "переключиться на лязг";)
Тот же вопрос в другом синтаксисе, stackoverflow.com/questions/28062095





Я не совсем уверен, можно ли это рассматривать как идею хорошо, но что-то, что я иногда делаю, когда чувствую себя ленивым, - это определение макроса "const cast", например:
#define CC(_VAR) ((const typeof(_VAR))(_VAR))
Это, конечно, предполагает, что у вас есть компилятор, поддерживающий расширение typeof. Используя эту (сомнительную) конструкцию, вы могли бы написать
const shape *fooshapes(const foo *f)
{
return CC(f->shapes);
}
В противном случае C обычно не приводит к неявному преобразованию в const. Вам нужно явно привести указатели к константным указателям.
Спасибо за Ваш ответ! Я должен был упомянуть, что явное приведение типов, конечно, возможно, но я стараюсь этого избегать - в основном это служит для того, чтобы заглушить предупреждения компилятора. Однако ваше последнее предложение неверно ... преобразование из T * в const T * неявно в C, поэтому я был удивлен, что это другое, когда T является типом массива.
@FelixPalmen Думаю, ты прав. Я сам заметил это несоответствие, отсюда и глупый макрос. Я просто предположил, что C не приводил указатели, а предупреждения GCC были немного ошибочными. Может, это только указатели на массивы?
Я немного расширил свой вопрос, извините за то, что вообще не написал больше - это не должно аннулировать ваш ответ, просто вы должны знать.
typeof не является стандартом C, используйте вместо него _Generic.
Это действительно та же проблема с двойным указателем, о которой вы говорите в вопросе.
Вы можете преобразовать указатель на T в указатель на const-T. Но const, примененный к массиву, определяет тип элемента, а не тип массива (C11 6.7.3.9), так что это не то, что вы здесь пытаетесь сделать. Вы не пытаетесь преобразовать указатель-массив-of-T в указатель-на-const-array-of-T, а скорее пытаетесь преобразовать в указатель-на-массив-константы-T, и эти два типа несовместимы в C.
Спасибо за Ваш ответ! Я предполагал что-то подобное, но §6.7.3 стр. 9 не говорит мне всего этого. Сам массив в любом случае является «своего рода константой», поэтому лазейки, такой как двойной указатель, здесь не существует. «эти два типа несовместимы в C.» <- просто потому, что они не указаны в «совместимых типах», или вы знаете, есть ли где-то указанная причина? Между тем ответ связан Bob__ кажется интересным. ...
Я немного расширил свой вопрос, извините за то, что вообще не написал больше - это не должно аннулировать ваш ответ, просто вы должны знать.
pointer to array not compatible to a pointer to 'const' array?
Да, они несовместимы, вы не можете изменить вложенный квалификатор const, int (*a)[42] несовместим с int const (*b)[42]связанные с, так как double ** несовместим с double const **
Is my assumption correct that this doesn't lead to a hole in const correctness?
Ну, вы добавляете квалификатор const, поэтому нет. Фактически, return non const не вызывает предупреждения и не нарушает корректность const в соответствии с тем же правилом, которое не позволяет вам добавлять const. Но такой код очень запутанный, но не неопределенное поведение.
shape *fooshapes(const foo *f)
{
return f->shapes;
}
Does the explicit cast violate the standard here?
Строго, Да, как сказал компилятор, типы несовместимы, однако я не думаю, что это вызовет ошибку в любом реальном классическом оборудовании.
Дело в том, что стандарт не гарантирует, что double *a и double const *b имеют одинаковый размер или даже одно и то же значение, а просто гарантирует, что a == b будет давать положительное значение. Вы можете продвигать любой double * в double const *, но, как я уже сказал, вы продвигаете его с помощью должен. При приведении вы ничего не продвигаете, потому что массив / указатель вложен. Так что это неопределенное поведение.
Это «вы просто не можете сделать это в C», вы можете спросить, но «что я должен делать?». Ну, я не вижу цели вашей структуры ни вашего указателя на массив в структуре. Чтобы решить более глубокую проблему, связанную со структурой данных, вам следует задать другой вопрос, в котором вы подробнее расскажете о том, какова ваша цель с этим кодом. Например, ваша функция может делать что-то вроде этого:
uint8_t const *fooshapes(const foo *f, size_t i)
{
return f->shapes[i];
}
«Ну, я не вижу цели вашей структуры ни вашего указателя на массив в структуре.» Я объяснил цель во введении к вопросу. Конечно, показанный код не является настоящим кодом, он максимально урезан. Ссылка, которую вы даете, проливает немного света, спасибо, это действительно кажется недостатком в спецификации C (в то время как C++ делает «правильные вещи»).
@FelixPalmen C++ теряет некоторую гибкость, чтобы делать «правильные вещи», это выбор: «Я хочу иметь некоторые личные данные, но предоставить их только для чтения внешнему миру». не объясняет, что вы хотите делать с этим кодом. В вашем MCVE есть дизайн-поток, поэтому я спрашиваю вас об этом. Невозможно помочь вам найти хорошее решение, не зная, какова реальная цель этой структуры данных. Исправить MCVE так же просто, как удалить весь код, поскольку он ничего не делает.
Остальные, очевидно, хорошо понимали цель. И обходной путь, предложенный Lundin, на самом деле послужит этой цели ... Я не очень доволен этим, но похоже, что это лучший вариант, предоставление C, похоже, не поддерживает то, что мне здесь нужно. Показанный код предназначен не для делать чего-то, а для разоблачать некоторых частных данных, доступных только для чтения.
"Когда вы бросаете, вы ничего не продвигаете, потому что массив / указатель вложен" Я сомневаюсь в этом рассуждении. С указатель на массив нет вложенного указателя ... кстати, разве вы не имели в виду перерабатывать?
@FelixPalmen Да, я думаю, что я достаточно хорошо понимаю код C, поэтому я хорошо понимаю вашу основную цель. Но вы явно не поняли мой последний пример, если ваша цель - раскрыть некоторые личные данные, я полагаю, вы хотите перебрать их, чтобы сделать что-то похожее на то, что я даю. Другой ответ, который говорит вам использовать его, легко принять, но это не делает ваш код хорошим. Я повторяю в последний раз, каков вариант использования этой функции с такими данными, держу пари, что вы где-то ошибаетесь. Promote / convert похожи, и да, я уверен в том, что сказал.
Теперь я наконец понял, к чему вы стремитесь. Если исходить из предположения, что вызывающий код хочет перебрать этот массив, ваше предложение имеет смысл, поэтому получите положительный отзыв :), к сожалению, объяснение всего моего проекта здесь намного превысит объем этого вопроса ... вызывающий код не будет повторяться но для результата используйте несколько memcpy().
@FelixPalmen "выходит за рамки этого вопроса" да, конечно. «вызывающий код не будет повторяться, но будет использовать некоторые функции memcpy () для результата». может быть, fooshapes() сам справится? Вроде этот fooshapes() не раскрывает свои внутренние данные.
Позвольте нам продолжить обсуждение в чате.
Проблема в том, что, определяя тип массива, а затем определяя const указатель на этот тип, вы фактически получаете const uint8_t(*)[64], который несовместим с uint8_t(*)[64]1). Правильность констант и указатели на массивы ведут себя неудобно вместе, посмотри это для примера одной и той же проблемы.
В любом случае, корень проблемы в этом конкретном случае скрывает массив за typedef. Обычно это не лучшая идея. Вы можете исправить это, вместо этого обернув массив внутри структуры, что также может улучшить общий дизайн. Пример:
typedef struct shape
{
uint8_t shape[64];
} shape_t;
typedef struct foo
{
shape_t shapes;
} foo_t;
Теперь вы можете просто вернуть const shape_t*.
При желании вы можете сделать shape_t непрозрачным, как и foo_t. Или вы можете сделать внутреннюю часть shape_t общедоступной, например, выставив объявление структуры в общедоступном заголовке shape.h.
1) Неявное преобразование между указателем на тип и квалифицированным указателем на тип - единственное разрешенное неявное преобразование.
C11 6.3.2.3/2
For any qualifier q, a pointer to a non-q-qualified type may be converted to a pointer to the q-qualified version of the type; the values stored in the original and converted pointers shall compare equal.
Здесь это не применяется. Чтобы преобразование было правильным, оно должно быть преобразованием из типа указатель в массив в тип из указателя в квалифицированный массив.
Но это не так, это преобразование из указателя в тип массива в тип квалифицированного указателя в массив.
Нормативный текст для совместимых типов в C - это глава 6.2.7, которая ссылается только на 6.7.3. Соответствующие части:
C11 6.7.3 / 9
If the specification of an array type includes any type qualifiers, the element type is so-qualified, not the array type.
и C11 6.7.3 / 10
For two qualified types to be compatible, both shall have the identically qualified version of a compatible type
Вот почему gcc правильно выдает диагностическое сообщение - указатели не имеют идентичных версий.
@Stargateur Да, я действительно предлагаю переписать код, удалив массив (указатель на).
В качестве примечания, использование приведения указателя массива тоже должно работать. Это было решение, которое несколько человек придумали в связанном вопросе, и я не понимаю, как оно нарушит стандарт или вызовет какие-либо проблемы с псевдонимом. Просто возвращение указателей на массивы из функций - это вообще довольно неприятная вещь.
Пример: #define const_cast(n, arr) _Generic(arr, int(*)[n] : (const int(*)[n])arr ) .
Это "критическое неопределенное поведение";)
@Stargateur Нет, поскольку эффективный тип исходного объекта не является константным типом. «Критическое неопределенное поведение» - это сначала приведение к константе, а затем снова отбрасывание константы.
Нет, вы неверно истолковали это, это не проблема констант, проблема в том, что ничто не запрещает const double * иметь тот же размер или даже то же значение double *, из-за этого вы не можете изменять вложенный указатель квалификатора.
@Stargateur Единственная проблема стандарта C с преобразованием указателя массива в квалифицированный указатель массива - 6.3.2.3/7. То есть выравнивание и т.д. Что касается вашего примера здесь, это неверно (и не та же проблема, что и в вопросе), поскольку 6.3.2.3/2 требует, чтобы double* сравнивался с const double*.
@Stargateur Потому что const uint8_t(*)[64] по сравнению с uint8_t(*)[64] - это не то же самое, что const double* по сравнению с double*. Последний (указатель на квалифицированные данные) на 100% покрывается 6.3.2.3/2. Первый не упоминается явно, потому что это квалифицированный указатель на данные, который не то же самое, что указатель на квалифицированные данные.
6.3.2.3/2: For any qualifier q, a pointer to a non-q-qualified type may be converted to a pointer to the q-qualified version of the type; the values stored in the original and converted pointers shall compare equal.
Спасибо за этот ответ! Хотя это не полностью проясняет ситуацию для меня (например, Зачем несовместимы ли эти типы? Причина, указанная для двойных указателей, здесь не существует ... и в этом случае литье тоже нормально?) - это дает очень ценная идея для «обходного пути», поэтому имейте положительный отзыв и благодарность :) Заключение моего объекта в «структуру» только для того, чтобы обойти странное ограничение C, мне не кажется идеальным, но это был бы возможный способ . Незначительная придирка, я бы не стал называть тип _t из-за того, что POSIX резервирует это пространство имен ...
Вы путаете все, что я сказал, нет смысла продолжать обсуждение.
Я добавил к ответу ссылки на стандарт, объясняя, почему есть предупреждение от gcc.
@Lundin тоже за это спасибо. Под Зачем я имел в виду мотивацию (например, есть наглядный пример для случая двойного указателя, когда неявное преобразование из T ** в const T ** пробивает брешь в гарантии const). Мне не хватает такой вещи для этого случая массива, но теперь предположим, что его просто не существует, и этот случай не был полностью продуман в спецификации C: o
@Lundin "В качестве примечания, использование приведения указателя массива тоже должно работать." Я тоже в этом почти уверен. Но нарушит ли это стандарт? Если да, я изменю свой код, чтобы использовать "обходной путь структуры" :)
@FelixPalmen Нет, не будет, как только вы используете явное приведение, единственная часть стандарта, которая применяется, - это 6.3.2.3/7, которая касается только фундаментальных вещей, таких как выравнивание и фактическое представление данных. И не должно быть никаких проблем со строгим псевдонимом, поскольку данные на самом деле представляют собой массив uint8_t.
Приведение не происходит автоматически, но вы можете это сделать. Создание указателей
constне делает ничего доступным только для чтения. Тот, кто вызывает вашу функцию, может просто преобразовать ее в неконстантный и изменить содержимое по своему желанию.