7

在尝试使用 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*。这让我想到了我遇到的问题: signedunsigned 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数组的读取和写入。这在不知道类的内部结构的情况下是可能的,只要它提供类型charGLchar2(但不是它们的指针)之间的隐式转换。charGLchar2或它们的指针类型之间不需要显式转换。

我并不是说这个实现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++ 标准的代码:

  1. 使用从 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();

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

第二种方法的优点是不需要显式转换,但要实现它,必须动态分配字符串空间。另一个潜在的缺点是 OpenGL 不一定会为输入和输出流运算符提供重载来处理它们的类型或指针类型。然而,正如我所展示的,只要至少实现了与 char 的类型转换,就可以自己编写这些重载。

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

现在我的问题是:我是否正确地考虑过这一点,以便我的代码对 OpenGL 所做的可能更改保持安全,并且 - 无论答案是是还是否 - 是否有更好(即更安全)的方法来确保向上兼容性我的代码?

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

我也不是在问如何编写一个提供隐式指针转换的类(尽管这将是一个有趣的练习)。这个示例类的重点是禁止隐式指针分配,因为如果 OpenGL 决定更改其实现,则无法保证会提供此类。

4

2 回答 2

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因为这不仅会对现有代码造成严重破坏,还会对 OpenGL-over-network 协议(如 GLX)造成严重破坏。

更新

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

更新 2

请注意,您将很难找到平台 ABI 具有的任何 C 实现CHAR_BIT != 8 并且存在针对它的 OpenGL 实现——哎呀,我什至不确定是否存在或根本没有任何 C 实现CHAR_BIT != 8int和的不寻常尺寸short?当然!但是炭?我不知道。

更新 3

关于将整个事情放入 C++ 静态类型系统中,我建议从类型、特征和分配器中派生一个自定义glstring类。当涉及到大多数 ABI 中的指针类型兼容性时,它们的别名和行为类似于标准 C 字符串。std::basic_stringGLcharGLcharsigned char

于 2015-01-03T21:01:09.480 回答
3

扩展@datenwolf 答案:

关于CHAR_BIT:C 需要CHAR_BIT >= 8,char是 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));

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

您用“GL 类型不是 C 类型”引用的段落是指 OpenGL 规范使用没有“GL”前缀的“float”和“int”等类型,因此它说尽管它使用了这些无前缀的名称,它们不(必然)引用相应的 C 类型。在具体的 C 语言绑定中,名为“int”的 OpenGL 类型可能是 C 类型“long”的别名。相反,任何合理的绑定都将使用 C 类型,以便您可以使用 OpenGL 类型编写算术表达式(在 C 中,您只能使用内置类型来执行此操作)。

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

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

编辑:

有平台CHAR_BIT > 8。请参阅标准委员会关心的奇异架构。虽然今天它主要限于 DSP。POSIX 要求CHAR_BIT == 8.

永远不要打扰实例化basic_stringsiostreams使用标准要求之外的类型。如果您的类型是其中一个的别名,那您很好,但您可以直接使用前者。如果您的类型不同,您将进入特征、语言环境、编解码器状态等的永无止境的噩梦,这些都无法便携解决。事实上,永远不要使用除 a 之外的任何东西char

于 2015-01-03T23:30:56.963 回答