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 月、Chapter 3.1 Character Set and Phases of Compilation) を参照できます。

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 (Chapter 30 Loading Text File Shaders) では、ソース コードがファイルから変数に読み込まれ (私の好みの方法) std::string shaderString、GL 文字列の初期化に使用されます。

const GLchar* shaderSource = shaderString.c_str();

私がこれまでに見た中で最も冒険的なアプローチは、シェーダー ファイルを Google でロードしたときに最初に得たものです。明示的なキャストを使用する OpenGL SDK でホストされているロードに関する ClockworkCoders チュートリアルではなく、GLchar*GLubyte*のようにします。

GLchar** ShaderSource;
unsigned long len;
ifstream file;
// . . .
len = getFileLength(file);
// . . .
*ShaderSource = (GLubyte*) new char[len+1];

まともな C++ コンパイラは、ここで無効な変換エラーを出します。g++ コンパイラは、-fpermissive フラグが設定されている場合にのみ、警告を出して実行します。そのようにコンパイルすると、最終的には と同じ長さの基本型のGLubyte単なるtypedefエイリアスであるため、コードは機能します。この場合、暗黙的なポインター変換によって警告が生成される可能性がありますが、それでも正しいことを行う必要があります。これは C++ 標準に反するため、 はorと互換性がないため、このようにするのは悪い習慣です。それは私が持っていた問題に私をもたらします: unsigned charcharchar*signedunsigned char*

私の要点は、これらのチュートリアルはすべて、OpenGL 仕様の実装が現在、基本的な型の typedef の形式での単なるウィンドウ ドレッシングであるという基本的な事実に依存しているということです。この仮定は、仕様ではまったくカバーされていません。さらに悪いことに、GL 型を C 型と見なすことは明らかに推奨されていません。

将来、何らかの理由で OpenGL の実装が変更され、それGLcharが の単純なtypedefエイリアスではなくなったchar場合、互換性のない型へのポインタ間の暗黙的な変換が行われないため、このようなコードはコンパイルされなくなります。場合によっては、無効なポインター変換を無視するようにコンパイラーに指示することは確かに可能ですが、そのような不適切なプログラミングへのゲートを開くと、コードに他のあらゆる種類の問題が発生する可能性があります。

私の理解では、それを正しく行っている場所を 1 つだけ見たことがあります。シェーダー コンパイルに関する公式の 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 のタイプ。

私の推論を説明するために、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(それらのポインターではない)の間の暗黙的な変換を提供する場合に限ります。charGLchar2またはそれらのポインター型の間で明示的な変換は必要ありません。

この の実装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;
}

GLcharC++ 標準に違反することなく常に文字列を正しく処理するコードを作成するには、少なくとも 2 つの実行可能な方法があると結論付けています。

  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

2 番目の方法には、明示的な変換が必要ないという利点がありますが、それを実装するには、文字列用のスペースを動的に割り当てる必要があります。もう 1 つの潜在的な欠点は、OpenGL が入力および出力ストリーム演算子の型またはポインター型を処理するためのオーバーロードを必ずしも提供しないことです。ただし、既に示したように、これらのオーバーロードを自分で作成することは、少なくとも char との間の型変換が実装されている限り、魔法の問題ではありません。

これまでのところ、c-string とまったく同じ構文を提供する、ファイルからの入力に対する実行可能なオーバーロードは他に見つかりませんでした。

さて、私の質問は次のとおりです。私のコードが OpenGL によって行われる可能性のある変更に対して安全なままであるように、これを正しく考えましたか?答えが「はい」か「いいえ」かに関係なく、上位互換性を確保するためのより良い (つまり、より安全な) 方法はありますか?私のコードの?

また、このstackoverflowの質問と回答を読みましたが、私が知る限り、文字列は基本的な型ではないため、カバーしていません。

また、暗黙的なポインター変換を提供するクラスを作成する方法も尋ねていません (ただし、それは興味深い演習になります)。この例のクラスのポイントは、実装を変更することを決定した場合に OpenGL がそのようなものを提供するという保証がないため、暗黙的なポインター割り当てを禁止することです。

4

2 に答える 2

3

@datenwolf の回答の拡張:

についてCHAR_BIT: C は を必要としCHAR_BIT >= 8charC でアドレス指定可能な最小単位であり、OpenGL には 8 ビット型があります。これは、システムに適合する OpenGL を実装できないことを意味しますCHAR_BIT != 8... ステートメントと一致しています

... 表 2.2 の正確なビット幅要件を満たすことができないアーキテクチャに GL API を実装することはできません。

OpenGL 4.5仕様から。

への変換GLubyte*によるとchar*、知る限り、実際には完全に有効な C および C++ です。char*他のすべての型に別名を付けることが明示的に許可されているため、次のようなコード

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

有効です。sizeof(char) == sizeof(GLchar) == 1OpenGL と C のビット幅要件を組み合わせることにより、 の配列として の配列に自由にアクセスできGLcharますchar

「GL タイプは C タイプではない」と引用した段落は、OpenGL 仕様が「GL」プレフィックスなしで「float」や「int」などのタイプを使用しているという事実に言及しているため、これらのプレフィックスのない名前を使用しているにもかかわらず、それらは (必然的に) 対応する C 型を参照しません。むしろ、「int」という名前の OpenGL 型は、具体的な C 言語バインディングにおける 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_stringsでください。iostreamsあなたのタイプがそれらのいずれかのエイリアスである場合、問題はありませんが、前者を直接使用できます。タイプが異なる場合、移植可能に解決できない特性、ロケール、codecvt 状態などの終わりのない悪夢に陥ります。実際、 a 以外は決して使用しないでcharください。

于 2015-01-03T23:30:56.963 に答える