Пытаясь разобраться в графическом программировании с использованием c ++ и OpenGL3 +, я натолкнулся на слегка специализированную проблему понимания с типом char, указателями на него и потенциальным неявным или явным преобразованием в другие типы указателей char. Я думаю, что смог найти решение, но я хотел бы перепроверить, попросив ваше мнение об этом.
Текущий (октябрь 2014) Спецификация профиля ядра 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, глава 3.1 Набор символов и этапы компиляции):
Исходный набор символов, используемый для языков затенения OpenGL, — это Unicode в схеме кодирования UTF-8.
Теперь способ, которым это реализовано в любом заголовке библиотеки OpenGL, который я хотел найти, является простым
typedef char GLchar;
что, конечно, идет вразрез с утверждением «GL-типы не являются C-типами», которое я только что процитировал.
Обычно это не будет проблемой, поскольку typedefs предназначены именно для такой ситуации, когда базовый тип может измениться в будущем.
Проблема начинается в пользовательской реализации.
Проходя несколько уроков по 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 ++ выдаст здесь недопустимую ошибку преобразования. Компилятор g ++ пропустит его с предупреждением, только если установлен флаг -fpermissive. Компилируя его таким образом, код будет работать, потому что GLubyte
в конце концов, просто typedef
псевдоним основного типа unsigned char
которая такой же длины как char
, В этом случае неявное преобразование указателя может генерировать предупреждение, но все равно должно делать правильные вещи. Это идет вразрез со стандартом C ++, где char*
является не совместим с signed
или же unsigned char*
так поступать так — плохая практика. Что подводит меня к моей проблеме:
Я хочу сказать, что все эти уроки основаны на том факте, что реализация спецификации OpenGL в настоящее время является просто оформлением витрин в форме typedef для основных типов. Это предположение никоим образом не охватывается спецификацией. Хуже того, явно не рекомендуется думать о типах GL как о типах C.
Если в какой-то момент в будущем реализация OpenGL должна измениться — по какой-либо причине — так, чтобы GLchar
уже не простой typedef
псевдоним char
код, подобный этому, больше не будет компилироваться, так как нет неявных преобразований между указателями на несовместимые типы. Хотя в некоторых случаях, безусловно, возможно сказать компилятору просто игнорировать недопустимое преобразование указателя, открытие ворот для плохого программирования может привести к множеству других проблем в вашем коде.
Я видел ровно одно место, которое делает это правильно, насколько я понимаю: официальный пример opengl.org вики по компиляции шейдеров, т.е.
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-string с нулевым символом в конце GLchar2
массивы. Это возможно без знания внутренней структуры класса, если она обеспечивает неявные преобразования между типами char
а также GLchar2
(но не их указатели). Нет явных преобразований между char
а также GLchar2
или их типы указателя необходимы.
Я не утверждаю, что эта реализация 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;
}
Я пришел к выводу, что есть по крайней мере два жизнеспособных способа написания кода, который всегда будет обрабатывать 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-строк.
Теперь мой вопрос это: правильно ли я продумал это, чтобы мой код оставался в безопасности от возможных изменений, внесенных OpenGL, и — независимо от того, ответ на этот вопрос — да или нет — есть ли лучший (то есть более безопасный) способ обеспечения совместимости моего кода с повышением ?
Также я прочитал этот stackoverflow вопрос и ответ, но, насколько мне известно, он не охватывает строки, поскольку они не являются фундаментальными типами.
Я также не спрашиваю, как написать класс, который обеспечивает неявные преобразования указателей (хотя это было бы интересным упражнением). Задача этого примера класса состоит в том, чтобы запретить неявное присвоение указателя, поскольку нет гарантии, что OpenGL предоставит такое, если они решат изменить свою реализацию.
Расширяя ответ @datenwolf:
относительно CHAR_BIT
: C требует CHAR_BIT >= 8
, char
является наименьшей адресуемой единицей в C, а OpenGL имеет 8-битный тип. Это означает, что вы не можете реализовать соответствующий OpenGL в системе с CHAR_BIT != 8
… что согласуется с утверждением
… Невозможно реализовать GL API в архитектуре, которая не может удовлетворить точные требования к ширине бит в таблице 2.2.
из спецификации 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 использует такие типы, как «float» и «int» без префикса «GL», поэтому в нем говорится, что, несмотря на использование этих префиксных имен, они не (обязательно) относятся к соответствующим типам C. Скорее тип OpenGL с именем «int» может быть псевдонимом типа C «long» в конкретной привязке языка C. Наоборот, любая вменяемая привязка будут используйте типы C, чтобы вы могли писать арифметические выражения с использованием типов OpenGL (в C вы можете делать это только со встроенными типами).
Правильно ли я продумал это, чтобы мой код оставался в безопасности от возможных изменений, внесенных OpenGL, и — независимо от того, был ли ответ «да» или «нет», — существует ли лучший (то есть более безопасный) способ обеспечения восходящей совместимости моего кода?
Я думаю, что вы слишком много думаете о переносимости кода с точки зрения языка-юриста, вместо того, чтобы сосредоточиться на изучении OpenGL и написании переносимого кода на практике. Спецификация OpenGL не определяет языковые привязки, но никакая привязка C никогда не нарушит то, что все ожидают от работы, как назначение const GLchar *str = "hello world"
, Помните также, что это С привязки, которые вы обычно используете из C ++, поэтому в заголовках не будет сумасшедших классов и перегрузок операторов, что практически ограничивает реализацию использованием фундаментальных типов для Таблицы 2.2.
Редактировать:
Есть платформы с CHAR_BIT > 8
, Увидеть Экзотические архитектуры, о которых заботятся комитеты по стандартам. Хотя сегодня это в основном ограничивается DSP. POSIX требует CHAR_BIT == 8
,
Никогда не беспокойтесь о создании basic_strings
а также iostreams
с типами, отличными от тех, которые требуются стандартом. Если ваш тип является псевдонимом одного из них, у вас все в порядке, но вы можете использовать первый напрямую. Если ваш тип отличается, вы будете вводить бесконечный кошмар черт, локалей, состояний codecvt и т. Д., Которые не могут быть разрешены переносимым образом. по факту никогда не используйте ничего, кроме char
.
Что спецификация OpenGL означает с заявлением
«Типы GL не являются типами C»
в том, что реализация OpenGL может использовать любой тип, который она считает подходящим для этой цели. Это не означает, что в реализации запрещено использование C-типов. Это означает, что при программировании с использованием OpenGL API не следует делать никаких предположений относительно природы типов OpenGL.
OpenGL указывает, что GLchar равен 8 битам (с явно не указанной подписью). Период, без дальнейшего обсуждения. Поэтому, пока вы кодируете свою программу таким образом, что GLchar обрабатывается как 8-битный тип данных, все в порядке. Если вы беспокоитесь о достоверности, вы можете добавить статическое утверждение CHAR_BIT == 8
в коде, чтобы выдать ошибку, если платформа не следует этому.
Типовые определения в заголовках OpenGL (заголовки не являются нормативными BTW) выбираются так, чтобы результирующие типы соответствовали требованиям базовой платформы ABI. Немного более портативный gl.h может сделать
#include <stdint.h>
typedef int8_t GLchar;
но это просто сводится к определению типа int8_t
который скорее всего будет
typedef signed char int8_t;
для обычного компилятора.
Если в какой-то момент в будущем реализация OpenGL должна измениться — по какой-либо причине — так, чтобы GLchar больше не был простым псевдонимом typedef для char, подобный код больше не будет компилироваться, так как не существует неявных преобразований между указателями в несовместимые типы.
OpenGL не определен с точки зрения C API или ABI. GLchar — 8 бит, и пока привязки API придерживаются этого, все в порядке. Будет никогда случается, что спецификация OpenGL меняется на другой размер для GLchar
потому что это нанесло бы ущерб не только существующему коду, но и протоколам OpenGL по сети, таким как GLX.
Обратите внимание, что если вы заботитесь о подписи. Наиболее важный эффект подписи в C связан с правилами целочисленного продвижения, а в C многие операции с символами фактически выполняются int
скорее, чем char
s (используя отрицательные значения в качестве побочного канала) и неудивительно, что правила целочисленного продвижения char
Тип С подписан. Вот и все.
Обратите внимание, что вам будет сложно найти любую реализацию C, для которой платформа ABI имеет CHAR_BIT != 8
а также Реализации OpenGL существуют для этого — черт, я даже не уверен, что есть или была любая реализация C с CHAR_BIT != 8
совсем. Необычные размеры для int
а также short
? Конечно! Но чар? Я не знаю.
Что касается встраивания всего этого в систему статических типов C ++, я бы предложил получить glstring
класс от std::basic_string
с типом, чертами и распределителем, который создается для GLchar
, Когда дело доходит до совместимости типов указателей в большинстве ABI GLchar
псевдонимы signed char
и таким образом ведет себя как стандартные строки C.