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

How can I safely ensure a char* type will be correctly implemented (on any platform) according to OpenGL spec?

在尝试使用 C++ 和 OpenGL3+ 进行图形编程时,我遇到了一个关于 char 类型、指向它的指针以及到其他 char 指针类型的潜在隐式或显式转换的稍微专业化的理解问题。我想我已经找到了解决方案,但我想通过询问您对此的看法来仔细检查。

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

GL types are not C types. Thus, for example, GL type int is referred to as GLint outside this document, and is not necessarily equivalent to the C type int. An implementation must use exactly the number of bits indicated in the table to represent a GL type.

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

The source character set used for the OpenGL shading languages is Unicode in the UTF-8 encoding scheme.

现在这在任何 OpenGL 库中实现的方式 header 我关心的是一个简单的

typedef char GLchar;

这当然与我刚刚引用的声明 "GL types are not C types" 背道而驰。

通常情况下,这不是问题,因为 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 加载着色器文件时得到的第一个方法 - 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 最后只是基本类型 unsigned chartypedef 别名,其长度与 char 相同。在这种情况下,隐式指针转换可能会产生警告,但仍然应该做正确的事情。这违反了 C++ 标准,其中 char* signedunsigned char* 兼容,所以这样做是不好的做法。这让我想到了我遇到的问题:

我的观点是,所有这些教程都依赖于一个基本事实,即 OpenGL 规范的 实现 目前只是 window 以 typedefs 的形式修饰基础类型。该假设决不包含在规范中。更糟糕的是,明确不鼓励将 GL 类型视为 C 类型。

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

根据我的理解,我只看到一个地方:官方 opengl.org wiki 关于着色器编译的示例,即:

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

与其他教程的唯一区别是在赋值之前 显式 强制转换为 const GLchar*。丑陋,我知道,但据我所知,它使代码安全,不受 OpenGL 规范(总结)的任何有效未来实现的影响:一种表示 UTF-8 编码方案中字符的位大小 8 的类型。

为了说明我的推理,我编写了一个简单的 class 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 != '[=16=]'; ++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 = '[=16=]'; // terminate with null character
  i.width(0); // reset width of i
  return i;
}

请注意,除了编写 class 之外,我还实现了输入和输出流运算符的重载,以正确处理来自 class 和 c-string 的读取和写入样式 null-terminated GLchar2 数组。这在不知道 class 的内部结构的情况下是可能的,只要它提供类型 charGLchar2 之间的隐式转换(但不是它们的指针)。 charGLchar2 或它们的指针类型之间不需要显式转换。

我并不是说 GLchar 的这个实现是值得的或完整的,但它应该用于演示目的。将它与 typedef char GLchar1; 进行比较,我发现我可以做什么,不能做什么h 这种类型:

// 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 = '[=17=]';
    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 = '[=17=]';
    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 != '[=17=]') ++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 != '[=17=]') ++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, '[=17=]');
    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, '[=17=]');
    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, '[=17=]');
    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数组中。

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

到目前为止,我还没有发现任何其他可行的文件输入重载,这些文件提供与 c-strings.

完全相同的语法。

现在我的问题是这样的:我是否正确地考虑过这一点,以便我的代码在 OpenGL 可能进行的更改和 - 无论答案是否定的情况下保持安全否 - 是否有更好(即更安全)的方法来确保我的代码向上兼容?

此外,我已阅读 this Whosebug 问答,但据我所知,它不包括字符串,因为它们不是基本类型。

我也不是在问如何编写确实提供隐式指针转换的 class(尽管那将是一个有趣的练习)。这个例子的要点 class 是禁止隐式指针赋值,因为如果他们决定改变他们的实现,不能保证 OpenGL 会提供这样的东西。

OpenGL 规范对语句的含义

"GL types are not C types"

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

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

选择 OpenGL headers 中的类型定义(headers 不是规范的 BTW),以便生成的类型符合底层平台 ABI 的要求。稍微更便携的 gl.h 可能会做 a

#include <stdint.h>
typedef int8_t GLchar;

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

typedef signed char int8_t;

对于通常的编译器。

If at any point in the future the OpenGL implementation should change - for whatever reason - so that GLchar is no longer a simple typedef alias of char, code like this will no longer compile as there are no implicit conversions between pointers to incompatible types

OpenGL 不是根据 C API 或 ABI 定义的。 GLchar 是 8 位,只要 API 绑定符合这一点,一切都很好。 永远不会 发生 OpenGL 规范为 GLchar 更改为不同大小的情况,因为这不仅会对现有代码造成严重破坏,还会对像 GLX 这样的 OpenGL-over-network 协议造成严重破坏.

更新

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

更新 2

请注意,您很难找到平台 ABI 具有 CHAR_BIT != 8 OpenGL 实现的任何 C 实现 – 哎呀,我'我什至不确定是否存在或曾经有任何带有 CHAR_BIT != 8 的 C 实现。 intshort 尺寸异常?当然!但是炭?我不知道。

更新 3

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

扩展@datenwolf 回答:

关于CHAR_BIT:C要求CHAR_BIT >= 8char是C中最小的可寻址单元,OpenGL有8位类型。这意味着您不能在具有 CHAR_BIT != 8... 的系统上实现符合标准的 OpenGL... 这与声明

一致

... it is not possible to implement the GL API on an architecture which cannot satisfy the exact bit width requirements in table 2.2.

来自 OpenGL 4.5 规范。

根据将 GLubyte* 转换为 char*,据我所知,它实际上是完全有效的 C 和 C++。 char* 被明确允许别名所有其他类型,这就是为什么代码像

int x;
istream &is = ...;
is.read((char*)&x, sizeof(x));

有效。由于 sizeof(char) == sizeof(GLchar) == 1 由 OpenGL 和 C bit-width 要求相结合,您可以自由访问 GLchar 数组作为 char.

数组

你用 "GL types are not C types" 引用的段落指的是 OpenGL 规范使用像 "float" 和 "int" 这样的类型而没有 "GL" 前缀,因此它说尽管它使用了这些无前缀的名称,但它们并不(必然)指代相应的 C 类型。相反,名为 "int" 的 OpenGL 类型可能是具体 C 语言绑定中 C 类型 "long" 的别名。相反,任何理智的绑定 使用 C 类型,以便您可以使用 OpenGL 类型编写算术表达式(在 C 中,您只能使用 built-in 类型)。

Have I thought this through correctly so that my code will remain safe against possible changes made by OpenGL and - no matter whether the answer is yes or no - is there a better (i.e. safer) way to ensure upward compatibility of my code?

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

使用基本类型的实现

编辑:

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

永远不要用标准要求以外的类型来实例化 basic_stringsiostreams。如果您的类型是其中之一的别名,那很好,但您可以直接使用前者。如果您的类型不同,您将进入永无止境的特征、区域设置、codecvt 状态等无法移植解决的噩梦。事实上 never use anything other than a char.