Слияние массивов через структуры

Я читаю параграф 7 из 6.5 в ISO/IEC 9899: TC2.

Он допускает доступ к объекту lvalue через:

совокупность или тип объединения, который включает один из вышеупомянутых типы среди своих членов (в том числе, рекурсивно, член subaggregate или contains union),

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

В разделе, отмеченном как

Цель этого списка - указать те обстоятельства, при которых объект может быть или не быть псевдонимом.

Я прочел это, сказав (например) следующее:

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

typedef struct {
    unsigned int x;
} s;

int main(void){
    unsigned int array[3] = {73,74,75};

   s* sp=(s*)&array; 

   sp->x=80;

   printf("%d\n",array[0]);

   return EXIT_SUCCESS;
}

Эта программа должна вывести 80.

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

Тем не менее, я не могу видеть очень вескую причину, чтобы запретить это. Мы знаем, что выравнивание и содержимое памяти в этом месте совместимы с sp->x, так почему бы и нет?

Похоже, что если я добавлю (скажем) a double y; к концу структуры, я все равно могу получить доступ к array[0] через sp->x таким образом.

Однако даже если массив больше, чем sizeof(s), любая попытка доступа к sp->y - это поведение "все ставки выключено" undefined.

Могу ли я вежливо просить людей сказать, что это предложение оправдывает, а не идти в плоский спин, выкрикивая "строгий псевдоним UB строгое сглаживание UB", похоже, слишком часто является способом этих вещей.

Ответ 1

Ответ на этот вопрос рассматривается в предложении: Исправление правил для псевдонимов на основе типов, которые мы увидим, к сожалению, не было разрешено в 2010 году, когда это предложение было сделанный, который покрыт Hedquist, Bativa, ноябрь 2010 минут. Поэтому C11 не содержит разрешения для DR 1520, поэтому это открытая проблема:

Кажется, что это не так, разрешенных на этом заседании. Каждый поток предлагаемых подходов приводит к большему количеству вопросов. 1 1:48, Чт, 4 ноября 2010 г.

ДЕЙСТВИЕ - Кларк больше работает в

DR 1520 открывается (подчеркивая мое продолжение):

Ричард Хансен указал на проблему с псевдонимом на основе типа правила, следующим образом:

Мой вопрос касается формулировки пули 5 6.5p7 (aliasing, как это применимо к объединениям/агрегатам). Если мое понимание эффективный тип неверен, он кажется объединенным/агрегатным условие должно применяться к эффективному типу, а не к типу lvalue.

Вот еще несколько деталей:

В качестве примера возьмем следующий фрагмент кода:

union {int a; double b;} u;
u.a = 5;

Из моего понимания определения эффективного типа (6.5p6) эффективный тип объекта в местоположении & u является объединением {int a; double b;}. Тип выражения lvalue, которое обращается к объект в & u (во второй строке) является int.

Из моего понимания определения совместимого типа (6.2.7) int несовместимо с union {int a; double b;}, поэтому пули 1 и 2 6.5p7 не применяются. int не является подписанным или беззнаковый тип типа объединения, поэтому пули 3 и 4 не применяются. ИНТ не является типом символа, поэтому пуля 6 не применяется.

Это оставляет пулю 5. Однако int не является агрегатом или объединением типа, так что пуля также не применяется. Это означает, что вышеупомянутое код нарушает правило псевдонимов, что явно не должно быть.

Я считаю, что пулю 5 следует перефразировать, указав, что если эффективный тип (не тип lvalue) - это совокупность или тип объединения который содержит элемент с типом, совместимым с типом lvalue, тогда объект может быть доступен.

Фактически, он указывает, что правила асимметричны в отношении членства в структуре/союзе. Я знал об этом ситуации, и считал это (не срочной) проблемой, для довольно некоторых время. Ряд примеров лучше иллюстрирует проблему. (Эти примеры изначально были представлены на встрече Санта-Крус.)

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

И соответствующий пример, применимый к этой ситуации, будет 3, который выглядит следующим образом:

struct S { int a, b; };
void f3(int *pi, struct S *ps1, struct S const *ps2)
{
  for (*pi = 0; *pi < 10; ++*pi) {
      *ps1++ = *ps2;
  }
}

Вопрос в том, можно ли получить доступ к объекту * ps2 (и особенно измененный) путем присвоения lvalue * pi - и если да, действительно ли стандарт говорит об этом. Можно утверждать, что это не охвачены пятой пулей 6.5p7, так как * pi не имеет агрегатный тип вообще.

Возможно, намерение состоит в том, чтобы вопрос был повернут: это позволило получить доступ к значению объекта * pi по lvalue * ps2. Очевидно, что этот случай будет покрыт пятой пулей.

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

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

Таким образом, казалось бы, стандартная формулировка, по-видимому, разрешает сценарий, который демонстрирует OP, хотя я понимаю, что это было непреднамеренным, и поэтому я бы избегал этого, и он мог бы потенциально изменить в более поздних стандартах несоответствие.

Ответ 2

Я признаю, что идея, что я могу разместить struct над локально определенным массивом таким образом, откровенно экзотична. Я все еще утверждаю, что C99 и все последующие стандарты позволяют это. Если факт очень спорен, что члены, являющиеся объектами сами по себе, первая маркерная точка в 6.7.5 позволяет:

тип, совместимый с эффективным типом объекта

Я думаю, что Мэтт Макнабб указывает.

Если посмотреть на проблему другим способом, то заметите, что она абсолютно законна (в строго соответствующей среде) для псевдонима члена sp->x как объекта в своем собственном праве.

В контексте кода в моем OP рассмотрим функцию с прототипом void doit(int* ip,s* sp);, ожидается, что следующий вызов будет вести себя логически:

doit(&(sp->x),sp);

Примечание: программная логика может (конечно) не работать должным образом. Например, если doit увеличивает sp->x, пока не превысит *ip, тогда возникнет проблема! Однако то, что не разрешено в компиляторе-совместителе, заключается в том, что результат искажается артефактами из-за того, что оптимизатор игнорирует потенциал псевдонимов.

Я утверждаю, что C будет все слабее, если язык требует от меня кода:

int temp=sp->x;
doit(&temp,sp);
sp->x=temp;

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

Очевидно, что жесткий оптимизирующий (то есть несоответствующий) компилятор может сделать полный хэш doit(), если он не признает, что ip может быть псевдонимом члена в середине sp. Это не имеет отношения к этой дискуссии.

Чтобы указать, когда компилятор может (и не может) делать такие предположения, понимается как причина, по которой стандарт должен устанавливать очень точные параметры вокруг сглаживания. То есть дать оптимизатору некоторые условия для диспропорции. На языке низкого уровня, таком как "C", может быть разумным (даже желательно) сказать, что подходящий выровненный указатель на доступный действительный шаблон бита может использоваться для доступа к значению.

Совершенно установлено, что sp->x в моем OP указывает на правильно выровненное местоположение, содержащее действительный unsigned int.

Интеллектуальные проблемы заключаются в том, согласны ли компилятор/оптимизатор, что тогда законный способ доступа к этому местоположению или незначителен как поведение undefined.

Как показывает пример doit(), он абсолютно определен, что структуру можно разбить и рассматривать как отдельные объекты, которые просто имеют особые отношения.

Этот вопрос, по-видимому, касается обстоятельств, когда набор членов, которые имеют такие особые отношения, может иметь структуру, "наложенную на них".

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

Он работает именно таким образом - он создает член структуры членом, затем обращается к нему через эту структуру.

Я подозреваю, что некоторые люди, которые возражают против кода в OP, более расслаблены. Во-первых, он работает с памятью, выделенной из бесплатного хранилища, в качестве "непечатаемого" универсально выровненного хранилища. Во-вторых, он строит целую структуру. В OP я указываю правила (по крайней мере, по-видимому, разрешают), что вы можете выровнять биты структуры и до тех пор, пока вы только отменяете эти биты, все в порядке.

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

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

Однако я скажу, что что-то вроде этого - единственное, что я мог придумать, когда такой подход кажется целесообразным. Но, с другой стороны, если вы не можете вытащить данные отдельно AND/OR, поместите их вместе, то вы быстро начнете нарушать понятие в структурах C, это POD - возможно дополненная сумма их частей, ничего больше, не меньше.

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

typedef enum {
    is_int, is_double //NB:TODO: support more types but this is a toy.

} type_of;

//This function allocates and 'builds' an array based on a provided set of types, offsets and sizes.
//It a stand-in for some function that (say) reads structures from a file and builds them according to a provided
//recipe. 
int buildarray(void**array,const type_of* types,const size_t* offsets,size_t mems,size_t sz,size_t count){
    const size_t asize=count*sz;
    char*const data=malloc(asize==0?1:asize);
    if(data==NULL){
        return 1;//Allocation failure.
    }
    int input=1;//Dummy...
    const char*end=data+asize;//One past end. Make const for safety!
    for(char*curr=data;curr<end;curr+=sz){
        for(size_t i=0;i<mems;++i){
            char*mem=curr+offsets[i];
            switch(types[i]){
                case is_int:
                    *((int*)mem)=input++;//Dummy...Populate from file...
                break;
                case is_double:
                    *((double*)mem)=((double)input)+((double)input)/10.0;//Dummy...Populate from file...
                    ++input;
                break;
                default:
                    free(data);//Better than returning an incomplete array. Should not leak even on error conditions.
                    return 2;//Invalid type!
            }
        }
    }
    if(array!=NULL){
        *array=data;
    }else{
        free(data);//Just for fun apparently...
    }
    return 0;
}

typedef struct {
    int a;
    int b;
    double c;
} S;

int main(void) {
    const type_of types[]={is_int,is_int,is_double};
    const size_t offsets[]={offsetof(S,a),offsetof(S,b),offsetof(S,c)};
    S* array=NULL;
    const size_t size=4;

    int err=buildarray((void **)&array,types,offsets,3,sizeof(S),size);
    if(err!=0){
        return EXIT_FAILURE;
    }
    for(size_t i=0;i<size;++i){
        printf("%zu: %d %d %f\n",i,array[i].a,array[i].b,array[i].c);
    }

    free(array);
    return EXIT_SUCCESS;
}

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

Раздел 6.5 стандарта C99 пытается (и не совсем преуспевает) установить эту границу.

Ответ 3

Я думаю, что этот текст не применяется:

тип агрегата или объединения, который включает один из вышеупомянутых типов среди его членов (в том числе, рекурсивно, член субагрегата или содержащегося объединения),

sp->x имеет тип unsigned int, который не является агрегатным или объединенным типом.

В вашем коде нет строгого нарушения псевдонимов: нормально читать unsigned int как unsigned int.

Структура может иметь разные требования к выравниванию для массива, но кроме этого нет проблем.

Доступ через "совокупность или тип объединения" будет:

s t = *sp;