🐥

神話のプログラム言語 Odin ポインタと配列編

2024/10/27に公開

Odinプログラム言語も、日に日に知名度が上がって来たのか、いつの間にかOdinタグに、アイコンマークが付くようになってました!、Zenn公式さん、ありがとう。
でも、Odinの記事を書いてるの、私だけなんですけどね。(笑)

今回はOdinのポインタと配列について、記載していきます。
Odinはデータ指向プログラム言語であるため、オブジェクトもクラスもメソッドもありません。ポインタと配列が、Odinプログラムの全てなのです。

【文字列の概念について、神からのお告げがありました】

神はおっしゃいました!!、文字列の概念については、多くの言語設計者は誤解してるよ!、文字列は以下の3つで構成されているだよ。
1.文字列の値 (string または const char *)
2.文字列の構築
3.文字列のバッファ ([]byte または char *)
ただ、多くのガベージ・コレクション言語では上記文字列を一つの文字列と捉えてしまうから、その文字列を変更した時(reallocなど)にバグの元になるんだよ!
gingerBill氏の文字列型の区別
Odinじゃ文字列値とバイト配列の区別を明確にしてんだよ!と、神のお告げを賜りました。

実際、Odin言語では、全てのファイル読み込み関数ではByteバッファ([]byte)で返されます。Odin言語では、string型はオブジェクトではなく、単なるrawptr+lenなのです。これはC言語も同様で、文字列はchar *で表せるのと同じです。

ポインタ

ポインタには、ポインタ・スライス・マルチポインタ・rawポインタの4種類があります。

  • ポインタは、^型名称のように型名の前に「^」が付きます。
  • スライスは、[]型名称型名称[:]型名称[数字:数字]の配列のような[]が付いた物です。配列に似ていますが、配列への参照みたいな物と覚えた方がいいでしょう。
  • マルチポインタは、[^]型名称で表現されます、ポインタ演算の代替手段として使われたり、外部コードとのインターフェイス、または非常に低レベルの処理を行う場合にのみ役立つそうです。[^]型(マルチポインタ)は、相対ポインタだと思います、相対ポインタを使用した算術処理は処理速度が向上するらしいですが、詳しくは、また別の記事にて記載します。
  • rawポインタは、rawptrと表示され、任意の型のポインタを意味します。C言語のvoid *と同意です。

それぞれ変換方法が異なるため、下記表にて記載します。

型名称 変換後の型 ポインタ変換 string変換
ポインタ ^string &str [:] スライスに変換
スライス []byte transmute([]byte)str string() string型に変換
マルチポインタ [^]byte raw_data(str) cstring型にキャスト変換
rawポインタ rawptr str: rawptr (^[]byte)(str)

※ちなみに、ポインタと言う意味では、OdinではC++11から導入されたスマートポインタは存在しません。スマートポインタはメモリアロケート時の解放が不必要になる利点がありますが、その分オーバヘッドをあるため、Odinでは集中型メモリ管理のような手段でメモリを管理しています。

下記にポインタを使ってstringに変換するプログラムを記載して説明します。

1.ポインタ

プロジェクト直下に「ptr01」ディレクトリを作成し、以下のようにwindows_setting.odinとmain.odinファイルを作成します。

ptr01/windows_setting.odin
package main

import "core:sys/windows"

// @(init)はmainより先に実行すると言う意味です。
// main関数にWindows特有の問題を入れるのが嫌だったので、外部ファイルにしました。
@(init) output_UTF8 :: proc() { windows.SetConsoleOutputCP(windows.CODEPAGE.UTF8) }

※windows_setting.odinファイルは、WindowsOSでの日本語文字化け対応ですので、他のOSを利用している方は、必要ありません。また、今後は、このwindows_setting.odinは記載しませんが、main.odinがあるディレクトリには、このファイルが入ってると思ってください。

ptr01/main.odin
package main
import "core:fmt"

main :: proc() {
  fmt.println("----- ポインタ(^string) -> string 変換 -----")
  str := "テスト"
  a := &str  // 変数strをポインタにして、変数aに渡す
  fmt.printf("^string: value=%v type=%v\n", a[:], type_info_of(type_of(a[:])))
}

string型の文字列を、普通のポインタはするには、&strで渡せば良いだけです。
ただ、再度、文字列として出力するのであれば、上記のように変数名[:]のようにスライスを付けなくてはいけません。

$ odin run .\ptr01\
----- ポインタ(^string) -> string 変換 -----
^string: value=テスト type=string

プロジェクト直下に「ptr01」ディレクトリを作成し、以下のようにmain.odinファイルを作成します。

ptr02/main.odin
package main
import "core:fmt"

main :: proc() {
  fmt.println("----- スライス([]byte) -> string 変換 -----")
  str := "テスト"
  a := transmute([]byte)str   // transmute変換演算子は、同じサイズの2つの型間のビットキャスト変換です
  fmt.printf("[]byte: value=%v type=%v\n", string(a), type_info_of(type_of(a)))
}

string型の文字列を、byteのスライスはするには、transmute変換演算子でキャスト変換しなければいけません。また、同様に、[]byteを文字列として認識するには、string(変数名)、またはstring(変数名[:])でstring変換する必要があります。

$ odin run .\ptr02\
----- スライス([]byte) -> string 変換 -----
[]byte: value=テスト type=[]u8

2.マルチポインタ

プロジェクト直下に「ptr03」ディレクトリを作成し、以下のようにmain.odinファイルを作成します。

ptr03/main.odin
package main
import "core:fmt"
import "core:strings"

main :: proc() {
  fmt.println("----- マルチポインタ([^]byte) -> string 変換 -----")
  str := "テスト"
  a := raw_data(str)    // raw_data関数は[^]byteに変換する

  // マルチポインターは、cstring型にキャストすれば戻せる
  b := string( cast(cstring)a )
  fmt.printf("raw_data(str): value=%v type=%v\n", b, type_info_of(type_of(a)))

  // マルチポインターは、string_from_ptr関数を使えば戻せる
  c := strings.string_from_ptr(a, len(str))
  fmt.printf("raw_data(str): value=%v type=%v\n", c, type_info_of(type_of(a)))
}

マルチポインタは、raw_data関数を使って、[^]byteに変換できます。
マルチポインタは逆参照はサポートされていませんが、戻す方法は二通りあります。
一つは、cstringにキャストして戻す方法、二つ目は、stringsライブラリのstring_from_ptrを使えばstringに戻す事が出来ます。

$ odin run .\ptr03\
----- マルチポインタ([^]byte) -> string 変換 -----
raw_data(str): value=テスト type=[^]u8
raw_data(str): value=テスト type=[^]u8

3.rawポインタ

プロジェクト直下に「ptr04」ディレクトリを作成し、以下のようにmain.odinファイルを作成します。

ptr04/main.odin
package main
import "core:fmt"

main :: proc() {
  fmt.println("----- rawポインタ(rawptr) -> string 変換 -----")
  str := "テスト"
  a := &str           // 一旦string型を^stringに変更
  b: rawptr           // 任意の型のポインタを意味するrawptrを宣言
  b = a               // ^stringをrawptrに変更
  c := (^[]byte)(b)   // (^[]byte)(b)でbyte配列のポインタに変換
  fmt.printf("(^[]byte)(b) : value= %v type=%v\n", string(c[:]), type_info_of(type_of(b)))

  By :: struct { buf: [dynamic]byte }     // 構造体で動的byte配列を作成し
  by := (^By)(b)                          // rawptrを構造体のポインタでキャストした後に、string(by.buf[:])すればstringを得る事が出来る
  fmt.printf("(^struct)(b) : value= %v type=%v\n", string(by.buf[:]), type_info_of(type_of(b)))
}

rawポインタは、byte配列(正確にはbyteスライス)をポインタに変換した後に、string型にすれば戻す事が可能。または、動的配列の構造体を用意してポインタに変換する手段もあります。

$ odin run .\ptr04\
----- rawポインタ(rawptr) -> string 変換 -----
(^[]byte)(b) : value= テスト type=rawptr
(^struct)(b) : value= テスト type=rawptr

配列

Odinでは、配列を表す記述は4種類([数字]int[]int, [?]int, [dynamic]int)あります。
[]intはスライスですが、配列と似ているため、以下の表に記載します。
[?]intは、疑問符 (?) を使用してその長さを自動的に推測できます。
[dynamic]intは、動的配列はスライスに似ていますが、実行時に長さが変わる場合があります。
また、動的配列はサイズ変更可能で、現在のコンテキストのアロケータを使用して割り当てられます。

配列型 形式 alloc 初期設定 列の追加 配列演算
[数字]int 固定配列 可能 可能 不可 可能
[]int スライス 可能 可能 不可 不可
[?]int 固定配列 可能 可能 不可 可能
[dynamic]int 動的配列 可能 可能 可能 不可
[^]int マルチポインタ 可能 不可 不可 不可

[]intはint型のスライス(スライスは配列への参照のようなもので、データのポインターとスライスの長さを格納されています)は、初期設定も可能なため、ここでは配列として表現しています、またマルチポインタは、配列ではないが、アロケート出来るので、表に追加。

1.配列の初期化

プロジェクト直下に「arry01」ディレクトリを作成し、以下のようにmain.odinファイルを作成します。

arry01/main.odin
package main
import "core:fmt"

main :: proc() {
  // --- 配列の初期設定
  i   := []i32  {1, 2, 3, 4, 5}    // スライスの初期設定: [1, 2, 3, 4, 5]
  foo := [?]int {0..=3 = 1}        // 配列の初期設定   : [1, 1, 1, 1]
  bar := [?]int {0 = 0, 1..<3 = 1} // 配列の初期設定   : [0, 1, 1]
  favorite_animals := []string{
    0 = "Raven",
    1 = "Zebra",
    2 = "Spider",
    3..=5 = "Frog",
    6..<8 = "Cat"
  }
  fmt.println(favorite_animals)
}

配列は、初期値を設定する事が可能です。
部分的に設定したい場合は、{列番号 = 値}と記載し、同一値を複数入れる場合は、{最初の列番号 ..< 最後の列番号 = 値}と設定します。

$ odin run .\arry01\
["Raven", "Zebra", "Spider", "Frog", "Frog", "Frog", "Cat", "Cat"]

2.動的配列の追加・挿入

プロジェクト直下に「arry02」ディレクトリを作成し、以下のようにmain.odinファイルを作成します。

arry02/main.odin
package main
import "core:fmt"

main :: proc() {
  // 動的配列への追加
  x: [dynamic]int
  defer delete(x)           // 動的配列はアロケータによって割り当てられる為、フリーは必要

  append(&x, 123)           // 1数字を追加
  append(&x, 4, 1, 74, 3)   // 複数の数字を追加
  fmt.printf("x value=%v\n", x)

  y := [dynamic]int {1, 2, 3}
  defer delete(y)

  append(&y, ..x[:])       // スライスによる配列の追加
  fmt.printf("y value=%v\n", y)

  // 挿入割り当て
  z := make([dynamic]int, 0, 6) // 動的配列をサイズ0、キャパシティ6に設定
  defer delete(z)

  inject_at(&z, 0, 10)         // 0列目に10を設定
  inject_at(&z, 3, 10)         // 3列目に10を設定
  fmt.printf("z1 value=%v len=%v cap%v\n", z[:], len(z), cap(z)) // [10, 0, 0, 10] 4 6

  assign_at(&z, 3, 20)         // 3列目に20を設定
  assign_at(&z, 4, 30)         // 4列目に30を設定
  fmt.printf("z2 value=%v len=%v cap=%v\n", z[:], len(z), cap(z)) // [10, 0, 0, 20, 30] 5, 6

  assign_at(&z, 5, 40, 50, 60)  // 5列目から複数値を挿入
  fmt.printf("z3 value=%v len=%v cap=%v\n", z[:], len(z), cap(z)) // [10, 0, 0, 20, 30, 40, 50, 60] 8 8
}

動的配列はアロケートされた配列であるため、追加・挿入を行う事が可能です。但し、[dynamic]型名称で変数を宣言した場合は、必ずdelete関数でメモリ解放してください。
メモリ割り当てのmake関数には、サイズ指定とキャパシティ指定があります。サイズは、割り当てサイズの初期サイズで、キャパシティは最大容量を表します。キャパシティ以上の追加を行った場合、自動でキャパシティサイズを増加します。

$ odin run .\arry02\
x value=[123, 4, 1, 74, 3]
y value=[1, 2, 3, 123, 4, 1, 74, 3]
z1 value=[10, 0, 0, 10] len=4 cap=6
z2 value=[10, 0, 0, 20, 30] len=5 cap=6
z3 value=[10, 0, 0, 20, 30, 40, 50, 60] len=8 cap=8

3.配列演算

プロジェクト直下に「arry03」ディレクトリを作成し、以下のようにmain.odinファイルを作成します。

arry03/main.odin
package main
import "core:fmt"

main :: proc() {
  // 配列の単一演算
  a := [2][3]int {
    {1, 2, 3},
    {4, 5, 6}
  }
  a += 10
  fmt.println(a)   // [[11, 12, 13], [14, 15, 16]]

  // 配列の同列演算
  b := [2][3]int {
    {7, 8, 9},
    {10, 11, 12}
  }
  fmt.println(a+b) // [[18, 20, 22], [24, 26, 28]]

  // 配列の単一演算
  c: [100]int      // 変数を宣言した時から、値は0に設定されます。
  c += 2
  fmt.println(c)   // [2, 2, 2, 2, 2, 2,・・・]
}

配列演算は、二次元配列でも可能です。

$ odin run .\arry03\
[[11, 12, 13], [14, 15, 16]]
[[18, 20, 22], [24, 26, 28]]
[2, 2, 2, 2, 2, 2, 2, 2, 2,・・・, 2]

おわりに

ポインタを文章で表現するのは、本当に難しい。値が格納されたメモリ上のアドレスがポインタなのに、何故そんなにいくつも変換方法があるの?って思われるかもしれませんが、これはもう慣れるしか方法はないかと思います。
Odin言語でプログラムを作った時に、これどうやって変換するんだろ?って悩んだ時に、この記事を見返して頂ければ、幸いです。

Discussion