Каков результат NULL + int? - PullRequest
18 голосов
/ 27 ноября 2011

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

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

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

Ответы [ 4 ]

31 голосов
/ 27 ноября 2011

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

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

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

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

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

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

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

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

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

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

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

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

26 голосов
/ 27 ноября 2011
#define BUFFER_OFFSET(i) ((char *)NULL + (i))

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

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

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

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

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

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

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

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

Итак, все сводится к тому, чтобы как-то обойти сигнатуру старой функции.Приведение указателя - грязный метод ИМХО.Я бы сделал это немного по-другому: я бы связался с сигнатурой функции:

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

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

3 голосов
/ 27 ноября 2011

Это не «NULL + int», это «NULL приведение к типу указатель на символ», а затем увеличивает этот указатель на i.

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

2 голосов
/ 27 ноября 2011

Данные атрибута вершины 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 
);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...