Что является результатом NULL + int?

Я видел, как в реализациях OpenGL VBO используется следующий макрос:

#define BUFFER_OFFSET(i) ((char *)NULL + (i))
//...
glNormalPointer(GL_FLOAT, 32, BUFFER_OFFSET(x));

Не могли бы вы подробно рассказать о том, как работает этот макрос? Можно ли заменить его функцией? Точнее, каков результат увеличения указателя NULL?

Ответы

Ответ 1

Возьмем поездку обратно через грязную историю OpenGL. Когда-то было OpenGL 1.0. Вы использовали glBegin и glEnd для рисования, и все. Если вам нужен быстрый рисунок, вы застряли в списке отображения.

Затем у кого-то была яркая идея, чтобы иметь возможность просто брать массивы объектов для рендеринга. И таким образом родился OpenGL 1.1, который принес нам такие функции, как glVertexPointer. Вы могли заметить, что эта функция заканчивается словом "Указатель". Это потому, что он принимает указатели на фактическую память, к которой будет обращаться, когда вызывается один из glDraw* наборов функций.

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

Однако, поскольку ARB придерживался абсолютной политики сделать все как можно более обратную совместимость (как бы глупо это выглядело API), они решили, что лучший способ реализовать это - снова использовать те же функции. Только теперь существует глобальный переключатель, который изменяет поведение glVertexPointer с "принимает указатель" на "принимает смещение байта от объекта буфера". Этот переключатель является тем, привязан ли объект буфера к GL_ARRAY_BUFFER.

Конечно, что касается C/С++, функция по-прежнему принимает указатель. И правила C/С++ не позволяют передавать целое число как указатель. Не без броска. Вот почему существуют макросы типа BUFFER_OBJECT. Это один из способов преобразования целочисленного байтового смещения в указатель.

Часть (char *)NULL просто принимает указатель NULL (обычно это void*) и превращает его в char*. + i просто выполняет арифметику указателя на char*. Поскольку NULL обычно является нулевым значением, добавление i к нему увеличит смещение байта на i, таким образом генерируя указатель, значение которого представляет собой смещение байта, которое вы передали.

Конечно, спецификация С++ перечисляет результаты BUFFER_OBJECT как поведение undefined. Используя это, вы действительно полагаетесь на компилятор, чтобы сделать что-то разумное. В конце концов, NULL не должно быть равным нулю; вся спецификация говорит, что это константа указателя нулевой указателя, определяемая реализацией. Он не должен иметь значение нуля вообще. В большинстве реальных систем это будет. Но это не обязательно.

Вот почему я просто использую трансляцию.

glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, (void*)48);

Это поведение undefined в любом случае. Но он также короче, чем набирать "BUFFER_OFFSET". GCC и Visual Studio, похоже, считают это разумным. И он не полагается на значение NULL-макроса.

Лично, если бы я был более педантичным С++, я бы использовал reinterpret_cast<void*> на нем. Но я не знаю.

Можете ли вы написать glVertexAttribBuffer, который принимает смещение, а не указатель? Абсолютно. Но это не такая уж большая сделка. Просто сделайте бросок.

Ответ 2

#define BUFFER_OFFSET(i) ((char *)NULL + (i))

Технически результатом этой операции является undefined, а макрос фактически ошибочен. Позвольте мне объяснить:

C определяет (и С++ следует за ним), что указатели могут быть записаны в целые числа, а именно типа uintptr_t, и что, если целое число, полученное таким образом, возвращенное в исходный тип указателя, из которого оно получено, даст оригинальный указатель.

Тогда есть арифметика указателя, что означает, что если у меня есть два указателя, указывающие на один и тот же объект, я могу различить их, в результате чего получается целое число (типа ptrdiff_t) и что целое число добавляется или вычитается в любой из оригинальные указатели, даст другой. Он также определяет, что добавлением 1 к указателю присваивается указатель на следующий элемент индексированного объекта. Также разница двух uintptr_t, деленная на sizeof(type pointed to) указателей того же объекта, должна быть равна вычитанию указателей. И последнее, но не менее важное: значения uintptr_t могут быть любыми. Они также могут быть непрозрачными ручками. Они не обязаны быть адресами (хотя большинство реализаций делают это так, потому что это имеет смысл).

Теперь мы можем посмотреть на печально известный нулевой указатель. C определяет указатель, который вызывается для из типа uintptr_u значение 0 как недопустимый указатель. Обратите внимание, что это всегда 0 в вашем исходном коде. На стороне сервера, в скомпилированной программе, двоичное значение, используемое для фактического представления его на машине, может быть чем-то совершенно другим! Обычно это не так, но может быть. С++ - это то же самое, но С++ не допускает столько же неявного литья, сколько C, поэтому нужно явно указать 0 на void*. Кроме того, поскольку нулевой указатель не ссылается на объект и поэтому не имеет разыменованной величины, то арифметика указателя размера undefined для нулевого указателя. Нулевой указатель, ссылающийся на какой-либо объект, также означает, что нет никакого определения для разумного перевода его на типизированный указатель.

Итак, если это все undefined, почему этот макрос работает в конце концов? Потому что большинство реализаций (значит, компиляторы) являются чрезвычайно доверчивыми, а компиляторы - ленивыми в высшей степени. Целочисленное значение указателя в большинстве реализаций - это просто значение самого указателя на стороне бэкэнда. Таким образом, нулевой указатель на самом деле равен 0. И хотя арифметика указателя на нулевом указателе не проверяется, большинство компиляторов молча примет его, если указатель получил определенный тип, даже если это не имеет смысла. char - это тип "единицы измерения размера", если вы хотите это сказать. Таким образом, арифметика указателя на актерском исполнении подобна артигемике по адресам на стороне сервера.

Чтобы сделать длинный рассказ короче, просто не имеет смысла пытаться делать волшебство указателя с предполагаемым результатом, чтобы быть смещением на стороне языка C, он просто не работает таким образом.

Позвольте отступить на мгновение и вспомнить, что мы на самом деле пытаемся сделать. Первоначальная проблема заключалась в том, что функции gl…Pointer принимают указатель как свой параметр данных, но для объектов буфера Vertex мы действительно хотим указать байт-смещение в наши данные, которое является числом. Для компилятора C функция принимает указатель (непрозрачная вещь, как мы узнали). Правильным решением было бы введение новых функций, особенно для использования с VBOs (скажем gl…Offset - я думаю, что я буду ралли для их введения). Вместо этого OpenGL определил, как работают компиляторы. Указатели и их целочисленный эквивалент реализуются как одно и то же двоичное представление большинством компиляторов. Итак, что мы должны сделать, это делает компилятор для вызова этих gl…Pointer функций с нашим числом вместо указателя.

Итак, технически единственное, что нам нужно сделать, это сообщить компилятору "да, я знаю, что вы думаете, что эта переменная a является целым числом, и вы правы, а функция glVertexPointer принимает только void* для это параметр данных. Но угадайте, что: Это целое было получено из void*", отбросив его до (void*), а затем удерживая большие пальцы, что компилятор на самом деле настолько глуп, чтобы передать целочисленное значение, как это было в glVertexPointer.

Итак, все это сводится к тому, чтобы как-то обойти старую сигнатуру функции. Литье указателя - это грязный метод IMHO. Я бы сделал это немного по-другому: я бы включил подпись функции:

typedef void (*TFPTR_VertexOffset)(GLint, GLenum, GLsizei, uintptr_t);
TFPTR_VertexOffset myglVertexOffset = (TFPTR_VertexOffset)glVertexPointer;

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

Ответ 3

Это не "NULL + int", а указатель "NULL cast to the type" на char ", а затем увеличивает этот указатель на i.

И да, это может быть заменено на функцию, но если вы не знаете, что она делает, то почему вас это волнует? Сначала поймите, что он делает, а затем подумайте, было бы лучше как функция.

Ответ 4

Данные атрибута vertex openGL назначаются через ту же функцию (glVertexAttribPointer) как указатели в памяти или смещения, расположенные в объекте буфера вершин, в зависимости от контекста.

появляется макрос BUFFER_OFFSET() для преобразования целочисленного байтового смещения в указатель, чтобы позволить компилятору безопасно передавать его как аргумент указателя. "(char *) NULL + i" выражает это преобразование посредством указателя-арифметики; результатом должен быть тот же бит-шаблон, предполагающий sizeof (char) == 1, без которого этот макрос завершится с ошибкой.

это также было бы возможно благодаря простому повторному кастингу, но макрос мог бы сделать его стилистически понятным, что передается; это также было бы удобным местом для перехвата переполнения для 32/64-битной безопасности/будущей безопасности

struct MyVertex { float pos[3]; u8 color[4]; }
// general purpose Macro to find the byte offset of a structure member as an 'int'

#define OFFSET(TYPE, MEMBER) ( (int)&((TYPE*)0)->MEMBER)

// assuming a VBO holding an array of 'MyVertex', 
// specify that color data is held at an offset 12 bytes from the VBO start, for every vertex.
glVertexAttribPointer(
  colorIndex,4, GL_UNSIGNED_BYTE, GL_TRUE,
  sizeof(MyVertex), 
  (GLvoid*) OFFSET(MyVertex, color) // recast offset as pointer 
);