我如何安全地确保根据OpenGL规范正确实现char *类型(在任何平台上)?

时间:2015-01-03 20:33:55

标签: c++ opengl type-conversion type-safety char-pointer

在尝试使用c ++和OpenGL3 +进行图形编程时,我遇到了一个稍微专业化的理解问题,包括char类型,指向它的指针以及潜在的隐式或显式转换到其他char指针类型。我想我已经找到了一个解决方案,但我想通过要求你对此进行双重检查。

当前(2014年10月)OpenGL4.5 core profile specification(2.2章命令语法中的表2.2)列出了OpenGL数据类型并明确说明了

  

GL类型不是C类型。因此,例如,GL类型int在本文档之外被称为GLint,并且不一定等同于C类型int。实现必须精确使用表中指示的位数来表示GL类型。

此表中的GLchar类型被指定为位宽8的类型,用于表示构成字符串的字符。
为了进一步缩小GLchar提供的范围,我们可以查看GLSL Specification OpenGL着色语言4.50 ,2014年7月,第3.1章字符集和编译阶段):

  

用于OpenGL着色语言的源字符集是UTF-8编码方案中的Unicode。

现在,我在任何OpenGL库头中实现的方式都是一个简单的

typedef char GLchar;

当然,面对陈述而言苍蝇#GL; 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();

我见过的最冒险的方法是我在google 加载着色器文件时获得的第一个 - 在使用显式强制转换的OpenGL SDK上托管的ClockworkCoders教程 - 而不是{ {1}}但GLchar* - 就像这样:

GLubyte*

任何像样的c ++编译器都会在这里给出无效的转换错误。只有在设置-fpermissive标志时,g ++编译器才会发出警告。通过这种方式进行编译,代码将起作用,因为GLchar** ShaderSource; unsigned long len; ifstream file; // . . . len = getFileLength(file); // . . . *ShaderSource = (GLubyte*) new char[len+1]; 最终只是基本类型GLubyte的{​​{1}}别名,其长度与typedef相同。在这种情况下,隐式指针转换可能会生成警告,但仍应该做正确的事情。这违反了C ++标准,其中unsigned char 与<{1}}或char不兼容,所以这样做是不好的做法。这让我想到了我遇到的问题:

我的观点是,所有这些教程都依赖于以下基本事实:OpenGL规范的实现目前只是基本类型的typedef形式的装饰。规范绝不涵盖此假设。更糟糕的是,明确不鼓励将GL类型视为C类型。

如果在将来的任何时候OpenGL实现应该更改 - 无论出于何种原因 - 以使char*不再是signed的简单unsigned char*别名,这样的代码将不会更长的编译,因为指向不兼容类型的指针之间没有隐式转换。虽然在某些情况下肯定可以告诉编译器忽略无效的指针转换,但是打开这样的错误编程的大门可能会导致代码中出现各种其他问题。

我已经看到了一个符合我理解的地方:关于着色器编译的官方opengl.org wiki示例,即:

GLchar

与其他教程的唯一区别是在分配之前显式强制转换为typedef。丑陋,我知道,但是,据我所知,它使代码安全,防止OpenGL规范的任何有效的未来实现(总结):一种代表UTF-8编码方案中字符的8位。

为了说明我的推理,我编写了一个简单的类char来满足此规范,但不再允许隐式指针转换为任何基本类型:

std::string vertexSource = //Get source code for vertex shader.
// . . .
const GLchar *source = (const GLchar *)vertexSource.c_str();

请注意,除了编写类之外,我还实现了输入和输出流操作符的重载,以正确处理类的读取和写入以及c-string样式的以null结尾的const GLchar*数组。只要它在类型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; } 之间提供隐式转换(但不是它们的指针),就可以在不知道类的内部结构的情况下实现这一点。 <{1}}和GLchar2或其指针类型之间没有明确的转换是必要的。

我不是声称char的这种实施是值得的或完整的,但它应该用于演示。将它与GLchar2进行比较我发现我能用这种类型做什么,不能做什么:

char

我的结论是,至少有两种可行的方法来编写能够在不违反C ++标准的情况下正确处理GLchar2字符串的代码:

  1. 使用从char数组到GLchar数组的显式转换(不整齐,但可行)。

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

  2. 使用输入流运算符将文件中的字符串直接读入GLchar数组。

  3. 第二种方法的优点是不需要显式转换,但要实现它,必须动态分配字符串的空间。另一个潜在的缺点是OpenGL不一定会为输入和输出流操作符提供重载来处理它们的类型或指针类型。但是,正如我已经表明的那样,只要至少实现了与char之间的类型转换,就自己编写这些重载并不是巫术。

    到目前为止,我还没有找到任何其他可行的重载来提供来自文件的输入,这些文件提供与c-strings完全相同的语法。

    现在我的问题是这样的:我是否正确地考虑了这一点,以便我的代码能够安全地防止OpenGL可能做出的更改 - 无论答案是肯定还是否 - 是否有更好(即更安全)的方式来确保我的代码向上兼容?

    另外,我已经阅读了this stackoverflow问题和答案,但据我所知,它不包括字符串,因为它们不是基本类型。

    我也没有问如何编写一个提供隐式指针转换的类(虽然这将是一个有趣的练习)。这个示例类的要点是禁止隐式指针赋值,因为如果他们决定改变它们的实现,就不能保证OpenGL会提供这样的指针。

2 个答案:

答案 0 :(得分:5)

OpenGL规范对语句

的意义
  

“GL类型不是C类型”

是,OpenGL实现可以使用它认为适合的任何类型。这并不意味着禁止实现使用C类型。这意味着在针对OpenGL API进行编程时,不必对关于OpenGL类型的性质做出任何假设。

OpenGL指定GLchar为8位(明确未指定签名)。期间,没有进一步的讨论。因此,只要您以某种方式编写程序,将GLchar视为8位数据类型,一切都很好。如果您担心有效性,可以在代码中添加静态断言CHAR_BIT == 8,以便在平台不遵循此规则时抛出错误。

选择OpenGL标头中的typedef(标头不是标准BTW),以便生成的类型符合底层平台ABI的要求。一个稍微便宜的gl.h可以做一个

#include <stdint.h>
typedef int8_t GLchar;

但这只是归结为int8_t的类型定义,可能只是

typedef signed char int8_t;

用于通常的编译器。

  

如果在将来的任何时候OpenGL实现应该更改 - 无论出于何种原因 - 以便GLchar不再是char的简单typedef别名,像这样的代码将不再编译,因为指针之间没有隐式转换不兼容类型

OpenGL没有根据C API或ABI定义。 GLchar是8位,只要API绑定符合这一点,一切都很好。如果OpenGL规范变为GLchar的不同大小,永远不会发生,因为这不仅会对现有代码造成严重破坏,还会对GLX等OpenGL网络协议造成严重破坏。

更新

请注意,如果您关心签名。 C中签名的最重要影响是关于整数提升规则,而在C中,许多字符操作实际上在int s而不是char s上运行(使用负值作为辅助通道)并且是对于整数提升规则不足为奇,C中的char类型已签名。就是这样。

更新2

请注意,您很难找到平台ABI为其存在CHAR_BIT != 8 OpenGL实现的任何C实现 - 哎呀,我甚至不确定,根本就有CHAR_BIT != 8的C实现。 intshort的不寻常尺寸?当然!但是char?我不知道。

更新3

关于将这一切都纳入C ++静态类型系统,我建议从glstring派生一个自定义std::basic_string类,其类型,特征和分配器为GLchar实例化。说到大多数ABI中的指针类型兼容性GLchar别名为signed char,因此表现得像标准C字符串。

答案 1 :(得分:2)

扩展@datenwolf回答:

关于CHAR_BIT:C需要CHAR_BIT >= 8char是C中最小的可寻址单位,OpenGL具有8位类型。这意味着您无法在具有CHAR_BIT != 8的系统上实现符合标准的OpenGL ...这与语句一致

  

...无法在无法满足表2.2中确切位宽要求的架构上实现GL API。

来自OpenGL 4.5规范。

根据将GLubyte*转换为char*,AFAIK实际上是完全有效的C和C ++。明确允许char*为所有其他类型设置别名,这就是

之类的代码
int x;
istream &is = ...;
is.read((char*)&x, sizeof(x));

有效。由于结合了OpenGL和C位宽度要求sizeof(char) == sizeof(GLchar) == 1,您可以自由地访问GLchar的数组作为char的数组。

您引用的段落&#34; GL类型不是C类型&#34;指的是OpenGL规范使用类似&#34; float&#34;等类型的事实。和&#34; int&#34;没有&#34; GL&#34;前缀,因此它表示,尽管它使用这些未加前缀的名称,但它们(必然)不会引用相应的C类型。而是一个名为&#34; int&#34;的OpenGL类型。可能是C类型的别名&#34; long&#34;在具体的C语言绑定中。相反,任何理智的绑定使用C类型,以便您可以使用OpenGL类型编写算术表达式(在C中,您只能使用内置类型)。

  

我是否正确地考虑了这一点,以便我的代码能够安全地防止OpenGL可能做出的更改 - 无论答案是肯定还是否 - 是否有更好(即更安全)的方法来确保我的代码向上兼容?

我认为你从语言 - 律师的角度思考代码可移植性太多,而不是专注于学习OpenGL和在实践中编写可移植的代码。 OpenGL规范没有定义语言绑定,但是没有C绑定会破坏每个人期望的工作,比如分配const GLchar *str = "hello world"。还要记住,这些是您通常在C ++中使用的 C 绑定,因此标题中不会出现疯狂的类和运算符重载,这实际上限制了实现使用表2.2的基本类型。

修改

CHAR_BIT > 8的平台。见Exotic architectures the standards committees care about。虽然今天它主要限于DSP。 POSIX需要CHAR_BIT == 8

永远不要用标准所要求的类型来实例化basic_stringsiostreams。如果你的类型是其中一个的别名,你很好,但你可以直接使用前者。如果您的类型不同,您将输入一个永无止境的特征,区域设置,代码状态等噩梦,这些都无法通过可移植的方式解决。事实上never use anything other than a char