NULL + 的结果是什么?整数?

发布于 2024-12-18 03:18:21 字数 225 浏览 0 评论 0原文

我已经看到在 OpenGL VBO 实现中使用了以下宏:

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

您能否提供有关该宏如何工作的一些详细信息?可以用函数代替吗? 更准确地说,增加 NULL 指针的结果是什么?

I have seen the following macro being used in OpenGL VBO implementations:

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

Could you provide a little detail on how this macro works? Can it be replaced with a function?
More exactly, what is the result of incrementing a NULL pointer?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

草莓味的萝莉 2024-12-25 03:18:21

让我们回顾一下 OpenGL 的肮脏历史。曾几何时,有 OpenGL 1.0。您使用 glBeginglEnd 来进行绘图,仅此而已。如果你想要快速绘图,你可以把东西放在显示列表中。

然后,有人想到了一个好主意,能够只使用对象数组进行渲染。于是OpenGL 1.1诞生了,它给我们带来了诸如glVertexPointer这样的函数。您可能会注意到该函数以单词“Pointer”结尾。这是因为它需要指向实际内存的指针,当调用 glDraw* 函数套件之一时将访问该内存。

再快进几年。现在,显卡能够自行执行顶点 T&L(到目前为止,固定功能 T&L 是由 CPU 完成的)。最有效的方法是将顶点数据放入 GPU 内存中,但显示列表并不理想。这些太隐蔽了,无法知道使用它们是否会获得良好的性能。输入缓冲区对象。

然而,由于 ARB 的绝对政策是使所有内容尽可能向后兼容(无论它使 API 看起来多么愚蠢),因此他们认为实现这一点的最佳方法就是再次使用相同的功能。只是现在,有一个全局开关将 glVertexPointer 的行为从“获取指针”更改为“获取缓冲区对象的字节偏移量”。该开关表示缓冲区对象是否绑定到 GL_ARRAY_BUFFER 。

当然,就C/C++而言,函数仍然需要一个指针。 C/C++ 的规则不允许您将整数作为指针传递。并非没有演员阵容。这就是像 BUFFER_OBJECT 这样的宏存在的原因。这是将整数字节偏移量转换为指针的一种方法。

(char *)NULL 部分只是获取 NULL 指针(在 C 中通常是 void* ,在 C++ 中通常是 0 )并将其转换为 字符*+ i 只是对 char* 进行指针算术。由于空指针通常具有零地址,因此向其添加 i 会将字节偏移量增加 i,从而生成一个指针,其值是您传入的字节偏移量。

当然,C++ 规范将 BUFFER_OBJECT 的结果列为未定义行为。通过使用它,您实际上依赖编译器来做一些合理的事情。毕竟,NULL 并不必须为零;规范只说它是一个实现定义的空指针常量。它的值根本不必为零。在大多数实际系统上,它会的。但它没有必须

这就是为什么我只使用演员表。

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

无论哪种方式都不能保证行为(int->ptr->int 转换是有条件支持的,不是必需的)。但它也比输入“BUFFER_OFFSET”要短。 GCC和Visual Studio似乎觉得很合理。而且它不依赖于 NULL 宏的值。

就我个人而言,如果我更学究 C++,我会在其上使用 reinterpret_cast 。但我不是。

或者您可以放弃旧的 API 并使用 glVertexAttribFormat 等。 al.,这在各方面都更好。

Let's take a trip back through the sordid history of OpenGL. Once upon a time, there was OpenGL 1.0. You used glBegin and glEnd to do drawing, and that was all. If you wanted fast drawing, you stuck things in a display list.

Then, somebody had the bright idea to be able to just take arrays of objects to render with. And thus was born OpenGL 1.1, which brought us such functions as glVertexPointer. You might notice that this function ends in the word "Pointer". That's because it takes pointers to actual memory, which will be accessed when one of the glDraw* suite of functions is called.

Fast-forward a few more years. Now, graphics cards have the ability to perform vertex T&L on their own (up until this point, fixed-function T&L was done by the CPU). The most efficient way to do that would be to put vertex data in GPU memory, but display lists are not ideal for that. Those are too hidden, and there's no way to know whether you'll get good performance with them. Enter buffer objects.

However, because the ARB had an absolute policy of making everything as backwards compatible as possible (no matter how silly it made the API look), they decided that the best way to implement this was to just use the same functions again. Only now, there's a global switch that changes glVertexPointer's behavior from "takes a pointer" to "takes a byte offset from a buffer object." That switch being whether or not a buffer object is bound to GL_ARRAY_BUFFER.

Of course, as far as C/C++ is concerned, the function still takes a pointer. And the rules of C/C++ do not allow you to pass an integer as a pointer. Not without a cast. Which is why macros like BUFFER_OBJECT exist. It's one way to convert your integer byte offset into a pointer.

The (char *)NULL part simply takes the NULL pointer (which is usually a void* in C and the literal 0 in C++) and turns it into a char*. The + i just does pointer arithmetic on the char*. Because the null pointer usually has a zero address, adding i to it will increment the byte offset by i, thus generating a pointer who's value is the byte offset you passed in.

Of course, the C++ specification lists the results of BUFFER_OBJECT as undefined behavior. By using it, you're really relying on the compiler to do something reasonable. After all, NULL does not have to be zero; all the specification says is that it is an implementation-defined null pointer constant. It doesn't have to have the value of zero at all. On most real systems, it will. But it doesn't have to.

That's why I just use a cast.

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

It's not guaranteed behavior either way (int->ptr->int conversions are conditionally supported, not required). But it's also shorter than typing "BUFFER_OFFSET". GCC and Visual Studio seem to find it reasonable. And it doesn't rely on the value of the NULL macro.

Personally, if I were more C++ pedantic, I'd use a reinterpret_cast<void*> on it. But I'm not.

Or you can ditch the old API and use glVertexAttribFormat et. al., which is better in every way.

想挽留 2024-12-25 03:18:21
#define BUFFER_OFFSET(i) ((char *)NULL + (i))

从技术上讲,此操作的结果是未定义,并且宏实际上是错误的。让我解释一下:

C 定义(C++ 遵循它),指针可以转换为整数,即 uintptr_t 类型,并且如果以这种方式获得整数,则将其转换回原始指针类型来自,会产生原始指针。

然后是指针算术,这意味着如果我有两个指针指向同一个对象,我可以取它们的差异,从而得到一个整数(类型为 ptrdiff_t ),并将该整数添加或减去原始指针的,将产生另一个。它还定义了,通过向指针加 1,将产生指向索引对象的下一个元素的指针。此外,两个uintptr_t的差除以同一对象的指针的sizeof(指向的类型)必须等于被减去的指针本身。最后但并非最不重要的一点是,uintptr_t 值可以是任何值。它们也可以是不透明的手柄。它们不需要是地址(尽管大多数实现都是这样做的,因为它有意义)。

现在我们可以看看臭名昭著的空指针。 C 定义从类型 uintptr_u 值 0 转换为无效指针的指针。 请注意,在源代码中这始终为 0。在后端,在编译后的程序中,用于实际向机器表示它的二进制值可能完全不同!通常不是,但也可能是。 C++ 是相同的,但 C++ 不允许像 C 那样多的隐式转换,因此必须将 0 显式转换为 void*。另外,由于空指针不引用对象,因此没有取消引用的大小空指针的指针算术未定义。空指针不引用任何对象也意味着,没有定义将其合理地转换为类型化指针。

那么,如果这一切都未定义,那么为什么这个宏仍然有效呢?因为大多数实现(意味着编译器)都非常容易上当受骗,而编译器编码人员则非常懒惰。大多数实现中指针的整数值只是后端指针本身的值。所以空指针实际上是 0。虽然没有检查空指针上的指针算术,但如果指针被分配了某种类型,大多数编译器都会默默地接受它,即使它没有任何意义。如果你想说的话,char 是 C 的“单位大小”类型。因此,强制转换上的指针算术就像后端地址上的算术一样。

长话短说,尝试用指针魔术将预期结果作为 C 语言端的偏移量是没有意义的,它就是行不通的。

让我们退后一步,记住我们实际上想要做的事情:最初的问题是,gl…Pointer 函数将指针作为其数据参数,但对于顶点缓冲区对象,我们实际上想在我们的数据中指定一个基于字节的偏移量,它是一个数字。对于 C 编译器来说,该函数需要一个指针(正如我们所知,这是一个不透明的东西)。正确的解决方案是引入新函数,特别是与 VBO 一起使用的函数(例如 gl…Offset - 我想我会为它们的引入而欢呼)。相反,OpenGL 定义的是对编译器工作方式的利用。大多数编译器将指针及其等效整数实现为相同的二进制表示形式。所以我们要做的就是让编译器用我们的数字而不是指针来调用那些 gl…Pointer 函数。

因此,从技术上讲,我们唯一需要做的就是告诉编译器“是的,我知道你认为这个变量 a 是一个整数,你是对的,并且该函数 glVertexPointer只需要一个 void* 作为它的数据参数。但猜猜看:该整数是从 void*" 中生成的,通过将其转换为 (void*)< /code> 然后举起大拇指,表示编译器将整数值传递给 glVertexPointer 实际上是很愚蠢的。

所以这一切都归结为以某种方式规避旧的函数签名。恕我直言,转换指针是肮脏的方法。我会做一些不同的事情:我会弄乱函数签名:

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

现在您可以使用 myglVertexOffset 而不进行任何愚蠢的转换,并且 offset 参数将被传递给函数,没有任何危险,编译器可能会弄乱它。这也是我在程序中使用的方法。

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

Technically the result of this operation is undefined, and the macro actually wrong. Let me explain:

C defines (and C++ follows it), that pointers can be casted to integers, namely of type uintptr_t, and that if the integer obtained that way, casted back into the original pointer type it came from, would yield the original pointer.

Then there's pointer arithmetic, which means if I have two pointers pointing so the same object I can take the difference of them, resulting in a integer (of type ptrdiff_t), and that integer added or subtracted to either of the original pointers, will yield the other. It is also defines, that by adding 1 to a pointer, the pointer to the next element of an indexed object is yielded. Also the difference of two uintptr_t, divided by sizeof(type pointed to) of pointers of the same object must be equal to the pointers themself being subtracted. And last but not least, the uintptr_t values may be anything. They could be opaque handles as well. They're not required to be the addresses (though most implementations do it that way, because it makes sense).

Now we can look at the infamous null pointer. C defines the pointer which is casted to for from type uintptr_u value 0 as the invalid pointer. Note that this is always 0 in your source code. On the backend side, in the compiled program, the binary value used for actually representing it to the machine may be something entirely different! Usually it is not, but it may be. C++ is the same, but C++ doesn't allow for as much implicit casting than C, so one must cast 0 explicitly to void*. Also because the null pointer does not refer to an object and therefore has no dereferenced size pointer arithmetic is undefined for the null pointer. The null pointer referring to no object also means, there is no definition for sensibly casting it to a typed pointer.

So if this is all undefined, why does this macro work after all? Because most implementations (means compilers) are extremely gullible and compiler coders lazy to the highest degree. The integer value of a pointer in the majority of implementations is just the value of the pointer itself on the backend side. So the null pointer is actually 0. And although pointer arithmetic on the null pointer is not checked for, most compilers will silently accept it, if the pointer got some type assigned, even if it makes no sense. char is the "unit sized" type of C if you want to say so. So then pointer arithmetic on cast is like artihmetic on the addresses on the backend side.

To make a long story short, it simply makes no sense to try doing pointer magic with the intended result to be a offset on the C language side, it just doesn't work that way.

Let's step back for a moment and remember, what we're actually trying to do: The original problem was, that the gl…Pointer functions take a pointer as their data parameter, but for Vertex Buffer Objects we actually want to specify a byte based offset into our data, which is a number. To the C compiler the function takes a pointer (a opaque thing as we learned). The correct solution would have been the introduction of new functions especially for the use with VBOs (say gl…Offset – I think I'm going to ralley for their introduction). Instead what was defined by OpenGL is a exploit of how compilers work. Pointers and their integer equivalent are implemented as the same binary representation by most compilers. So what we have to do, it making the compiler call those gl…Pointer functions with our number instead of a pointer.

So technically the only thing we need to do is telling to compiler "yes, I know you think this variable a is a integer, and you are right, and that function glVertexPointer only takes a void* for it's data parameter. But guess what: That integer was yielded from a void*", by casting it to (void*) and then holding thumbs, that the compiler is actually so stupid to pass the integer value as it is to glVertexPointer.

So this all comes down to somehow circumventing the old function signature. Casting the pointer is the IMHO dirty method. I'd do it a bit different: I'd mess with the function signature:

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

Now you can use myglVertexOffset without doing any silly casts, and the offset parameter will be passed to the function, without any danger, that the compiler may mess with it. This is also the very method I use in my programs.

我喜欢麦丽素 2024-12-25 03:18:21

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 
);

openGL vertex attribute data is assigned through the same function (glVertexAttribPointer) as either pointers in memory or offsets located within a Vertex Buffer Object depending on context.

the BUFFER_OFFSET() macro appears to convert an integer byte offset into a pointer simply to allow the compiler to pass it as a pointer argument safely.
The "(char*)NULL+i" expresses this conversion through pointer-arithmetic; the result should be the same bit-pattern assuming sizeof(char)==1, without which, this macro would fail.

it would also be possible through simple re-casting, but the macro might make it stylistically clearer what is being passed; it would also be a convenient place to trap overflows for 32/64bit safety/futureproofing

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 
);
相对绾红妆 2024-12-25 03:18:21

这不是“NULL+int”,而是“NULL 转换为‘指向 char 的指针’类型”,然后将该指针增加 i。

是的,它可以被一个函数取代 - 但如果你不知道它的作用,那你为什么要关心它呢?首先了解它的作用,然后考虑它作为一个函数是否会更好。

That's not "NULL+int", that's a "NULL cast to the type 'pointer to char'", and then increments that pointer by i.

And yes, that could be replaced by a function - but if you don't know what it does, then why do you care about that? First understand what it does, then consider if it would be better as a function.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文