Методы для преобразования функции типа void * `, несовместимой с типом в тип - PullRequest
2 голосов
/ 15 января 2020

Примечание. Этот вопрос пытается улучшить то, что я пытался задать здесь , но не смог.
Кроме того, я видел это и это . Они обсуждают похожие концепции, но не отвечают на эти вопросы.

Моя среда Windows 10, и для тестирования я использовал два компилятора, CLANG и G CC.

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

Ниже приведено упрощенное описание тестовой функции, которая вмещает несколько типов ввода, используя параметр void * и параметр перечисляемого значения, чтобы указать передаваемый тип .:

void func(void *a, int type)
{
    switch(type) {
        case CHAR://char
            char cVar1    = (char)a;      //compiles with no warnings/errors, seems to work
            char cVar2    = *(char *)a;   //compiles with no warnings/errors, seems to work
            break;
        case INT://int
            int iVar1     = (int)a;       //compiles with no warnings/errors, seems to work
            int iVar2     = *(int *)a;    //compiles with no warnings/errors, seems to work
            break;
        case FLT://float
            float fVar1   = (float)a;      //compile error:  (a1)(b1)
            float fVar2   = *(float *)a;   //requires this method
         case DBL://double
            double dVar1  = (double)a;     //compile error: (a1)(b1)(b2)
            double dVar2  = *(double *)a;//this appears to be correct approach
            break;
    };
}  

Метод вызова:

int main(void)
{

    char   c = 'P';
    int    d = 1024;
    float  e = 14.5;
    double f = 0.0000012341;
    double g = 0.0001234567;

    void *pG = &g;

    func(&c, CHAR);//CHAR defined in enumeration, typical
    func(&d, INT);
    func(&e, FLT);
    func(&f, DBL);
    func(pG, DBL);

    return 0;
}

Ниже приводится точный текст ошибки, относящийся к флагам в комментариях выше:

CLANG - версия 3.3

  • (a1) - ... ошибка: указатель не может быть приведен к типу 'float'

g cc - (tdm-1) 5.1.0

  • (b1) - ... ошибка: значение указателя, используемое там, где ожидалось значение с плавающей запятой
  • (b2) - ... ошибка: указатель не может быть приведен к типу 'double'

Для справки в обсуждении ниже

  • метод 1 == type var = (type)val;
  • метод 2 == type var = *(type *)val;

Мои результаты показывают, что для преобразования float & double требуется метод 2.
Но для char & int метод 2 кажется необязательным, то есть метод 1 компилируется нормально, и кажется, что ently.

Вопросы:

  • Казалось бы, для восстановления значения из аргумента функции void * всегда требуется метод 2, так почему Способ 1 (кажется) работает с типами char и int? Это неопределенное поведение?

  • Если метод 1 работает для char и int, почему он также не работает по крайней мере с типом float? Это не потому, что их размеры разные, то есть: sizeof(float) == sizeof(int) == sizeof(int *) == sizeof(float *). Это из-за строгого нарушения псевдонимов?

Ответы [ 3 ]

4 голосов
/ 15 января 2020

Стандарт C явно разрешает преобразования между указателями и целочисленными типами. Это описано в разделе 6.3.2.3, касающемся преобразования указателей:

5 Целое число может быть преобразовано в любой тип указателя. За исключением случаев, указанных ранее, результат определяется реализацией, может быть некорректно выровнен, может не указывать на объект ссылочного типа и может быть представлением прерывания.

6 Любой Тип указателя может быть преобразован в целочисленный тип. За исключением случаев, указанных ранее, результат определяется реализацией. Если результат не может быть представлен целочисленным типом, поведение не определено. Результат не обязательно должен находиться в диапазоне значений любого целочисленного типа.

Предполагая, что вы преобразуете целочисленный тип в void * при передаче его функции, а затем приводите его обратно к правильному целочисленному типу, это можно сделать при условии , что позволяет реализация. G CC, в частности, позволит это при условии, что рассматриваемый целочисленный тип по крайней мере такой же большой, как void *.

. Поэтому преобразование будет работать для случаев char и int, однако вам нужно будет передать значения (приведенные к void *) вместо адресов.

Так, например, если вы вызвали функцию следующим образом:

func4((void *)123, INT);

Тогда функция может сделать это:

int val = (int)a;

И val будет содержать значение 123. Но если бы вы назвали это так:

int x = 123;
func4(&x, INT);

Тогда val в функции будет содержать адрес x в main, преобразованный в целочисленное значение.

Преобразование между типом указателя и типом с плавающей запятой явно запрещено в соответствии с разделом 6.5.4p4 относительно оператора приведения:

Тип указателя не должен быть преобразован в какой-либо плавающий тип. Плавающий тип не должен быть преобразован в любой тип указателя.

Конечно, самый безопасный способ передачи значений через void * - это сохранить значение в переменной соответствующего типа, передать его адрес , затем приведите void * в функции обратно к правильному типу указателя. Это гарантированно работает.

1 голос
/ 15 января 2020

Казалось бы, для восстановления значения из аргумента функции void * всегда требуется метод 2, так почему же метод 1 (кажется) работает с типами char и int? Это неопределенное поведение?

Поскольку C специально разрешает преобразования между целыми числами и указателями. Это разрешено, поскольку может потребоваться express абсолютных адресов в виде целых чисел, особенно в программировании, связанном с аппаратным обеспечением. Результат может быть хорошим или может вызывать неопределенное поведение, подробности см. Ниже.

Когда вам нужно конвертировать между указателями и целыми числами, вы должны всегда использовать uintptr_t вместо этого для четко определенных и переносимых преобразований. Этот тип изначально не был частью C, поэтому преобразования в другие целочисленные типы все еще разрешены.

Если метод 1 работает для char и int, почему он также не работает с хотя бы тип с плавающей точкой? Это не потому, что их размеры разные, то есть: sizeof (float) == sizeof (int) == sizeof (int *) == sizeof (float *). Это из-за строгого нарушения псевдонимов?

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

Строгое алиасинг применяется только тогда, когда вы делаете "lvalue-доступ" к сохраненному значению. Вы делаете это, например, здесь: *(double *)a. Вы получаете доступ к данным через тип (double), совместимый с эффективным типом объекта (также double), так что это нормально.

(double *)a однако никогда не получает доступ к фактическим данным , но просто пытается преобразовать тип указателя во что-то еще. Строгий псевдоним не применяется.

Как правило, C допускает множество преобразований диких указателей, но у вас возникают проблемы только после того, как вы начнете де-ссылаться на данные через неверный тип. Именно тогда вы можете столкнуться с проблемами с невыразимым типом, смещением и строгим псевдонимом.


Подробности:

  • char c = 'P'; ... char cVar1 = (char)a;.
    Преобразование из типа указателя в целочисленный тип. Результат не определен или определяется реализацией 1) . Доступ к указанным данным по lvalue отсутствует, строгое алиасинг не применяется 2) .
  • char c = 'P'; ... char cVar2 = *(char *)a;.
    Доступ по lvalue символа через символ указатель. Совершенно четко определены 3) .
  • int d = 1024; ... int iVar1 = (int)a;.
    Преобразование из типа указателя в целочисленный тип. Результат не определен или определяется реализацией 1) . Доступ к указанным данным не происходит, строгое алиасинг не применяется 2) .

  • int d = 1024; ... int iVar2 = *(int *)a;
    Lvalue доступ к int через int указатель. Совершенно четкое определение 3) .

  • float e = 14.5; ... float fVar1 = (float)a;.
    Преобразование из типа указателя в плавающее. Несовместимое преобразование типов, нарушение ограничения оператора приведения 4) .

  • float e = 14.5; ... float fVar2 = *(float *)a;.
    Доступ к значению float через указатель float. Совершенно четкие 3) .

  • double ... аналогично float выше.


1) C17 6.3.2.3/6:

Любой тип указателя может быть преобразован в целочисленный тип. За исключением случаев, указанных ранее, результат определяется реализацией. Если результат не может быть представлен целочисленным типом, поведение не определено. Результат не обязательно должен находиться в диапазоне значений любого целочисленного типа.

2) C17 6.5 §6 и §7 . См. Что такое строгое правило псевдонимов?

3) C17 6.3.2.1 L-значения, массивы и обозначения функций и
C17 6.3.2.3/1:

Указатель на void может быть преобразован в или из указателя на любой тип объекта. Указатель на любой тип объекта может быть преобразован в указатель на void и обратно; результат должен сравниться с исходным указателем.

Кроме того, type идеально подходит для оценки доступа через (квалифицированный) указатель к type, C17 6.5 / 7 : «тип, совместимый с действующим типом объекта».

4) Ни одно из допустимых преобразований указателей, перечисленных в C17 6.3.2.3 . Нарушение ограничения C17 6.5.4 / 4 :

Тип указателя не должен быть преобразован в какой-либо плавающий тип. Плавающий тип не должен быть преобразован в любой тип указателя.

1 голос
/ 15 января 2020

На своих сайтах вызовов вы передаете адрес каждой переменной.

func4(&c, CHAR);
func4(&d, INT);
func4(&e, FLT);
func4(&f, DBL);
func4(pG, DBL);

(Это правильно.) Поэтому внутри func4 вы должен использовать то, что вы описываете как "метод 2":

T var1    = (T)a;    // WRONG, for any scalar type T
T var2    = *(T *)a; // CORRECT, for any scalar type T

Вы получили только ошибки времени компиляции для типов с плавающей точкой T, потому что стандарт C явно разрешает приведение из указатель на целочисленные типы. Но эти преобразования приводят к значению, которое имеет некоторую [определенную реализацией] связь с адресом переменной, предоставленной в качестве аргумента, а не с значением . Например,

#include <stdio.h>
int main(void)
{
    char c = 'P';
    printf("%d %d\n", c, (char)&c);
    return 0;
}

является допустимой программой, которая печатает два числа. Первое число будет 80, если вы не используете мэйнфрейм IBM. Второй номер непредсказуем. Это также может быть 80, но если это так, то это случайность, а не то, на что можно положиться. Это может быть даже не одно и то же число каждый раз, когда вы запускаете программу.

Я не знаю, что вы подразумеваете под «[метод 1], кажется, работает», но если вы на самом деле получили то же значение, которое вы передали в, это было чисто случайно. Метод 2 - это то, что вы должны делать.

...