在尝试使用 c++ 和 OpenGL3+ 进行图形编程时,我遇到了一个关于 char 类型、指向它的指针以及潜在的隐式或显式转换为其他 char 指针类型的稍微专业化的理解问题。我想我已经找到了解决方案,但我想通过询问您对此的看法来再次确认。
当前(2014 年 10 月)的OpenGL4.5 核心配置规范(第 2.2 章命令语法中的表 2.2)列出了 OpenGL 数据类型并明确说明
GL 类型不是 C 类型。因此,例如,GL 类型 int 在本文档之外称为 GLint,不一定等同于 C 类型 int。实现必须准确使用表中指示的位数来表示 GL 类型。
此表中的 GLchar 类型被指定为位宽为 8 的类型,用于表示构成字符串的字符。
为了进一步缩小 GLchar 必须提供的内容,我们可以查看GLSL 规范(OpenGL Shading Language 4.50,2014年 7 月,第 3.1 章字符集和编译阶段):
用于 OpenGL 着色语言的源字符集是 UTF-8 编码方案中的 Unicode。
现在,在我想要寻找的任何 OpenGL 库头文件中实现这一点的方式很简单
typedef char GLchar;
这当然违背了我刚刚引用的“GL 类型不是 C 类型”的陈述。
通常,这不会成为问题,因为 typedef 仅适用于底层类型可能在未来发生变化的情况。
问题始于用户实现。
通过一些关于 OpenGL 的教程,我遇到了各种将 GLSL 源代码分配给处理它所需的 GLchar 数组的方法。(请原谅我没有提供所有链接。目前,我没有这样做所需的声誉。)
网站 open.gl 喜欢这样做:
const GLchar* vertexSource =
"#version 150 core\n"
"in vec2 position;"
"void main() {"
" gl_Position = vec4(position, 0.0, 1.0);"
"}";
或这个:
// Shader macro
#define GLSL(src) "#version 150 core\n" #src
// Vertex shader
const GLchar* vertexShaderSrc = GLSL(
in vec2 pos;
void main() {
gl_Position = vec4(pos, 0.0, 1.0);
}
);
在lazyfoo.net(第 30 章加载文本文件着色器)上,源代码从文件(我的首选方法)中读取到std::string shaderString
变量中,然后用于初始化 GL 字符串:
const GLchar* shaderSource = shaderString.c_str();
我见过的最冒险的方法是我在谷歌加载着色器文件时得到的第一个方法——ClockworkCoders 加载托管在使用显式转换的 OpenGL SDK 上的教程——不是这样,GLchar*
而是GLubyte*
这样:
GLchar** ShaderSource;
unsigned long len;
ifstream file;
// . . .
len = getFileLength(file);
// . . .
*ShaderSource = (GLubyte*) new char[len+1];
任何体面的 c++ 编译器都会在此处给出无效的转换错误。仅当设置了 -fpermissive 标志时,g++ 编译器才会发出警告。以这种方式编译它,代码将起作用,因为GLubyte
最终只是typedef
基本类型的别名,其unsigned char
长度与char
. 在这种情况下,隐式指针转换可能会产生警告,但仍然应该做正确的事情。这违反了与or不兼容的C++ 标准,因此这样做是不好的做法char*
。这让我想到了我遇到的问题: signed
unsigned char*
我的观点是,所有这些教程都依赖于这样一个基本事实,即 OpenGL 规范的实现目前只是基本类型的 typedef 形式的装饰。规范绝不涵盖此假设。更糟糕的是,明确不鼓励将 GL 类型视为 C 类型。
如果在未来的任何时候 OpenGL 实现应该改变 - 无论出于何种原因 -GLchar
不再是 的简单typedef
别名char
,这样的代码将不再编译,因为指向不兼容类型的指针之间没有隐式转换。虽然在某些情况下当然可以告诉编译器忽略无效的指针转换,但打开这样的不良编程大门可能而且会导致代码中出现各种其他问题。
我已经看到了一个完全符合我理解的地方:关于 Shader Compilation 的官方 opengl.org wiki 示例,即:
std::string vertexSource = //Get source code for vertex shader.
// . . .
const GLchar *source = (const GLchar *)vertexSource.c_str();
与其他教程的唯一区别是在分配之前显式转换const GLchar*
。丑陋,我知道,然而,据我所知,它使代码对 OpenGL 规范(总结)的任何有效的未来实现都是安全的:一种位大小为 8 的类型,表示 UTF-8 编码方案中的字符。
为了说明我的推理,我编写了一个简单的类GLchar2
来满足这个规范,但不再允许隐式指针转换到任何基本类型或从任何基本类型转换:
// GLchar2.h - a char type of 1 byte length
#include <iostream>
#include <locale> // handle whitespaces
class GLchar2 {
char element; // value of the GLchar2 variable
public:
// default constructor
GLchar2 () {}
// user defined conversion from char to GLchar2
GLchar2 (char element) : element(element) {}
// copy constructor
GLchar2 (const GLchar2& c) : element(c.element) {}
// destructor
~GLchar2 () {}
// assignment operator
GLchar2& operator= (const GLchar2& c) {element = c; return *this;}
// user defined conversion to integral c++ type char
operator char () const {return element;}
};
// overloading the output operator to correctly handle GLchar2
// due to implicit conversion of GLchar2 to char, implementation is unnecessary
//std::ostream& operator<< (std::ostream& o, const GLchar2 character) {
// char out = character;
// return o << out;
//}
// overloading the output operator to correctly handle GLchar2*
std::ostream& operator<< (std::ostream& o, const GLchar2* output_string) {
for (const GLchar2* string_it = output_string; *string_it != '\0'; ++string_it) {
o << *string_it;
}
return o;
}
// overloading the input operator to correctly handle GLchar2
std::istream& operator>> (std::istream& i, GLchar2& input_char) {
char in;
if (i >> in) input_char = in; // this is where the magic happens
return i;
}
// overloading the input operator to correctly handle GLchar2*
std::istream& operator>> (std::istream& i, GLchar2* input_string) {
GLchar2* string_it;
int width = i.width();
std::locale loc;
while (std::isspace((char)i.peek(),loc)) i.ignore(); // ignore leading whitespaces
for (string_it = input_string; (((i.width() == 0 || --width > 0) && !std::isspace((char)i.peek(),loc)) && i >> *string_it); ++string_it);
*string_it = '\0'; // terminate with null character
i.width(0); // reset width of i
return i;
}
请注意,除了编写类之外,我还实现了输入和输出流运算符的重载,以正确处理类以及 c 字符串样式的空终止GLchar2
数组的读取和写入。这在不知道类的内部结构的情况下是可能的,只要它提供类型char
和GLchar2
(但不是它们的指针)之间的隐式转换。char
和GLchar2
或它们的指针类型之间不需要显式转换。
我并不是说这个实现GLchar
是值得的或完整的,但它应该是为了演示的目的。将其与 a 进行比较,typedef char GLchar1;
我发现我可以用这种类型做什么和不能做什么:
// program: test_GLchar.cpp - testing implementation of GLchar
#include <iostream>
#include <fstream>
#include <locale> // handle whitespaces
#include "GLchar2.h"
typedef char GLchar1;
int main () {
// byte size comparison
std::cout << "GLchar1 has a size of " << sizeof(GLchar1) << " byte.\n"; // 1
std::cout << "GLchar2 has a size of " << sizeof(GLchar2) << " byte.\n"; // 1
// char constructor
const GLchar1 test_char1 = 'o';
const GLchar2 test_char2 = 't';
// default constructor
GLchar2 test_char3;
// char conversion
test_char3 = '3';
// assignment operator
GLchar2 test_char4;
GLchar2 test_char5;
test_char5 = test_char4 = 65; // ASCII value 'A'
// copy constructor
GLchar2 test_char6 = test_char5;
// pointer conversion
const GLchar1* test_string1 = "test string one"; // compiles
//const GLchar1* test_string1 = (const GLchar1*)"test string one"; // compiles
//const GLchar2* test_string2 = "test string two"; // does *not* compile!
const GLchar2* test_string2 = (const GLchar2*)"test string two"; // compiles
std::cout << "A test character of type GLchar1: " << test_char1 << ".\n"; // o
std::cout << "A test character of type GLchar2: " << test_char2 << ".\n"; // t
std::cout << "A test character of type GLchar2: " << test_char3 << ".\n"; // 3
std::cout << "A test character of type GLchar2: " << test_char4 << ".\n"; // A
std::cout << "A test character of type GLchar2: " << test_char5 << ".\n"; // A
std::cout << "A test character of type GLchar2: " << test_char6 << ".\n"; // A
std::cout << "A test string of type GLchar1: " << test_string1 << ".\n";
// OUT: A test string of type GLchar1: test string one.\n
std::cout << "A test string of type GLchar2: " << test_string2 << ".\n";
// OUT: A test string of type GLchar2: test string two.\n
// input operator comparison
// test_input_file.vert has the content
// If you can read this,
// you can read this.
// (one whitespace before each line to test implementation)
GLchar1* test_string3;
GLchar2* test_string4;
GLchar1* test_string5;
GLchar2* test_string6;
// read character by character
std::ifstream test_file("test_input_file.vert");
if (test_file) {
test_file.seekg(0, test_file.end);
int length = test_file.tellg();
test_file.seekg(0, test_file.beg);
test_string3 = new GLchar1[length+1];
GLchar1* test_it = test_string3;
std::locale loc;
while (test_file >> *test_it) {
++test_it;
while (std::isspace((char)test_file.peek(),loc)) {
*test_it = test_file.peek(); // add whitespaces
test_file.ignore();
++test_it;
}
}
*test_it = '\0';
std::cout << test_string3 << "\n";
// OUT: If you can read this,\n you can read this.\n
std::cout << length << " " <<test_it - test_string3 << "\n";
// OUT: 42 41\n
delete[] test_string3;
test_file.close();
}
std::ifstream test_file2("test_input_file.vert");
if (test_file2) {
test_file2.seekg(0, test_file2.end);
int length = test_file2.tellg();
test_file2.seekg(0, test_file2.beg);
test_string4 = new GLchar2[length+1];
GLchar2* test_it = test_string4;
std::locale loc;
while (test_file2 >> *test_it) {
++test_it;
while (std::isspace((char)test_file2.peek(),loc)) {
*test_it = test_file2.peek(); // add whitespaces
test_file2.ignore();
++test_it;
}
}
*test_it = '\0';
std::cout << test_string4 << "\n";
// OUT: If you can read this,\n you can read this.\n
std::cout << length << " " << test_it - test_string4 << "\n";
// OUT: 42 41\n
delete[] test_string4;
test_file2.close();
}
// read a word (until delimiter whitespace)
test_file.open("test_input_file.vert");
if (test_file) {
test_file.seekg(0, test_file.end);
int length = test_file.tellg();
test_file.seekg(0, test_file.beg);
test_string5 = new GLchar1[length+1];
//test_file.width(2);
test_file >> test_string5;
std::cout << test_string5 << "\n";
// OUT: If\n
delete[] test_string5;
test_file.close();
}
test_file2.open("test_input_file.vert");
if (test_file2) {
test_file2.seekg(0, test_file2.end);
int length = test_file2.tellg();
test_file2.seekg(0, test_file2.beg);
test_string6 = new GLchar2[length+1];
//test_file2.width(2);
test_file2 >> test_string6;
std::cout << test_string6 << "\n";
// OUT: If\n
delete[] test_string6;
test_file2.close();
}
// read word by word
test_file.open("test_input_file.vert");
if (test_file) {
test_file.seekg(0, test_file.end);
int length = test_file.tellg();
test_file.seekg(0, test_file.beg);
test_string5 = new GLchar1[length+1];
GLchar1* test_it = test_string5;
std::locale loc;
while (test_file >> test_it) {
while (*test_it != '\0') ++test_it; // test_it points to null character
while (std::isspace((char)test_file.peek(),loc)) {
*test_it = test_file.peek(); // add whitespaces
test_file.ignore();
++test_it;
}
}
std::cout << test_string5 << "\n";
// OUT: If you can read this,\n you can read this.\n
delete[] test_string5;
test_file.close();
}
test_file2.open("test_input_file.vert");
if (test_file2) {
test_file2.seekg(0, test_file2.end);
int length = test_file2.tellg();
test_file2.seekg(0, test_file2.beg);
test_string6 = new GLchar2[length+1];
GLchar2* test_it = test_string6;
std::locale loc;
while (test_file2 >> test_it) {
while (*test_it != '\0') ++test_it; // test_it points to null character
while (std::isspace((char)test_file2.peek(), loc)) {
*test_it = test_file2.peek(); // add whitespaces
test_file2.ignore();
++test_it;
}
}
std::cout << test_string6 << "\n";
// OUT: If you can read this,\n you can read this.\n
delete[] test_string6;
test_file2.close();
}
// read whole file with std::istream::getline
test_file.open("test_input_file.vert");
if (test_file) {
test_file.seekg(0, test_file.end);
int length = test_file.tellg();
test_file.seekg(0, test_file.beg);
test_string5 = new GLchar1[length+1];
std::locale loc;
while (std::isspace((char)test_file.peek(),loc)) test_file.ignore(); // ignore leading whitespaces
test_file.getline(test_string5, length, '\0');
std::cout << test_string5 << "\n";
// OUT: If you can read this,\n you can read this.\n
delete[] test_string5;
test_file.close();
}
// no way to do this for a string of GLchar2 as far as I can see
// the getline function that returns c-strings rather than std::string is
// a member of istream and expects to return *this, so overloading is a no go
// however, this works as above:
// read whole file with std::getline
test_file.open("test_input_file.vert");
if (test_file) {
std::locale loc;
while (std::isspace((char)test_file.peek(),loc)) test_file.ignore(); // ignore leading whitespaces
std::string test_stdstring1;
std::getline(test_file, test_stdstring1, '\0');
test_string5 = (GLchar1*) test_stdstring1.c_str();
std::cout << test_string5 << "\n";
// OUT: If you can read this,\n you can read this.\n
test_file.close();
}
test_file2.open("test_input_file.vert");
if (test_file2) {
std::locale loc;
while (std::isspace((char)test_file2.peek(),loc)) test_file2.ignore(); // ignore leading whitespaces
std::string test_stdstring2;
std::getline(test_file2, test_stdstring2, '\0');
test_string6 = (GLchar2*) test_stdstring2.c_str();
std::cout << test_string6 << "\n";
// OUT: If you can read this,\n you can read this.\n
test_file.close();
}
return 0;
}
我得出的结论是,至少有两种可行的方法可以编写始终GLchar
正确处理字符串而又不违反 C++ 标准的代码:
使用从 char 数组到
GLchar
数组的显式转换(不整洁,但可行)。const GLchar* sourceCode = (const GLchar*)"some code";
std::string sourceString = std::string("some code"); // can be from a file GLchar* sourceCode = (GLchar*) sourceString.c_str();
使用输入流运算符将字符串从文件直接读取到
GLchar
数组中。
第二种方法的优点是不需要显式转换,但要实现它,必须动态分配字符串空间。另一个潜在的缺点是 OpenGL 不一定会为输入和输出流运算符提供重载来处理它们的类型或指针类型。然而,正如我所展示的,只要至少实现了与 char 的类型转换,就可以自己编写这些重载。
到目前为止,我还没有发现任何其他可行的文件输入重载,它提供与 c-strings 完全相同的语法。
现在我的问题是:我是否正确地考虑过这一点,以便我的代码对 OpenGL 所做的可能更改保持安全,并且 - 无论答案是是还是否 - 是否有更好(即更安全)的方法来确保向上兼容性我的代码?
另外,我已经阅读了这个stackoverflow 问题和答案,但据我所知,它不包括字符串,因为它们不是基本类型。
我也不是在问如何编写一个提供隐式指针转换的类(尽管这将是一个有趣的练习)。这个示例类的重点是禁止隐式指针分配,因为如果 OpenGL 决定更改其实现,则无法保证会提供此类。