LoginSignup
83
61

More than 1 year has passed since last update.

Cから学ぶRustの良さ

Last updated at Posted at 2022-09-01

背景

RustはしばしばCやC++言語と比較される記事が見られ、実際に、より開発がしやすいような機能が多々入っていると思います。そこで、私が以前業務で使用していたCと比較して、この点は、実際にRustを触ってみて良かったなと思った点をお伝えしたいと思います。
ただ、どちらかというと、Rustというより、あまり意識しなくても書けることは素晴らしいという話になるはず...


Cについて

Cは言わずもがな、今でも低レイヤーを扱ったりするときに用いられることもある言語です(ネットワーク、OSなど)。また、ハードウェアでも活躍しています。
マルチプラットフォームに対応しているので、環境に依存せずに実行でき、実行速度が速いことが特徴として挙げられます。
ただし、メモリを扱うことが多いため、習得難易度はやや高めとされているようです。


比較

Rustの概念として以下のようなものがあります。

  • 安全性
  • 並行性
  • 速度

今回は、安全性の部分を中心に話を広げます。


Cでの型宣言

C言語での型宣言は、宣言する型の領域を確保するものです。以下の例を見てみましょう。

C
#include<stdio.h>

int main() {
    int test; //ここでint型を扱うための4バイトの領域の取得
    test = 4;
    printf("%d\n", test);
}
実行結果
ussy@DESKTOP-91CH228:~/c$ gcc test.c -o test
ussy@DESKTOP-91CH228:~/c$ ./test 
4

また、Rustの例はこちらです。

Rust
fn main() {
    let n: i32 = 4;
    println!("{}", n);
}
実行結果
ussy@DESKTOP-91CH228:~/c$ rustc test.rs 
ussy@DESKTOP-91CH228:~/c$ ./test 
4

同じように見えますね。
確かに、書き方乗り違いはあれど、両方とも型を定義しているように見えます。
ところが...

C
#include<stdio.h>

int main() {
    int a;
    a = 'c';
    printf("[%c]\n", a);

    return 0;
}
実行結果
ussy@DESKTOP-91CH228:~/c$ gcc int.c -o int
ussy@DESKTOP-91CH228:~/c$ ./int 
[c]

なんでやねんって感じですね。
私がCを触り始めた最初に突っ込んだ場所です。
これは、C言語での型宣言は、intにバインドするものではなく、あくまで「int型(4byte)分の領域を取得するもの」だからです。char型は1byteなので、asciiなら4文字分入ります。
勿論、Rust(というか他の言語)では怒られます。

Rust
fn main() {
    let mut n: i32 = 4;
    n = 'c';
    println!("{}", n);
}
Rust
ussy@DESKTOP-91CH228:~/c$ rustc test.rs 
error[E0308]: mismatched types
 --> test.rs:3:9
  |
2 |     let mut n: i32 = 4;
  |                --- expected due to this type
3 |     n = 'c';
  |         ^^^ expected `i32`, found `char`
  |
help: you can cast a `char` to an `i32`, since a `char` always occupies 4 bytes
  |
3 |     n = 'c' as i32;
  |             ++++++

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.

素晴らしいですね。
私がC以外の言語を触るようになって感動した部分です。
 型が指定されること≒メモリを確保すること
というところがポイントです。


構造体

続いてCの構造体です。
Cでは、データを構造体という形で保持します。

C
struct strct {
    int in; // 4バイト
    char ch; // 1バイト
    char *str; // ?バイト
};

また、構造体を型として定義するときは以下の書き方です。

C
typedef struct {
    int i; // int型:4バイト
    char ch; // char型:1バイト
    char *str; // ?バイト
} Strct;

Cで構造体を考えるときは、上記のように何バイトあるかを計算できます。
因みにRustではこんな感じの書き方です。

Rust
struct Strct {
    i: i32,
    ch: char,
    st: String,
}

さて、ここで疑問が出てきますね。文字列は何バイトになるでしょう?
入力する文字の数によってバイト数が変わるのでしょうか?

実は、Cには文字列型なんて便利なものはありません。文字列というのは、char型の集合体と考えます。

ここで、char*と呼ばれるものが出てきます。
これは、Cでおなじみポインタと言われるものです。
説明を始めると余白が足りなくなるので、簡単にお伝えすると、これはメモリの番地を指す矢印を表します。
つまり、文字列の宣言は、「char型の集合の先頭の住所を格納する」というものです。
ポインタ型は32bitで4バイト、64bitで8バイトの大きさなので、これは8バイトの大きさになるわけですね!
これにより、文字列は別のメモリに格納して、その先頭アドレスを宣言することで、文字列を8バイトとして定義できます。
これでこの構造体が使用するメモリ領域を計算できるようになります。

構造体が使用するメモリの計算例は以下のようになります。

#include<stdio.h>

// 12+4バイト
typedef struct details {
    float tall; // 4バイト
    char* from; // 8バイト
} person_details;

// 28+4バイト
struct person {
    char *name; // 8バイト
    int age; // 4バイト
    person_details details; // 16バイト
};

int main() {
    person_details ussy_details = {
        174.8,
        "Saga"
    };
    struct person ussy = {
        "ushijima",
        28,
        ussy_details
    };

    printf("name: %s, from: %s, age: %d, tall: %.1f\n", ussy.name, ussy.details.from, ussy.age, ussy.details.tall);
    printf("size of pointer: %ld byte\n", sizeof(char *));
    printf("size of person_details: %ld byte\n", sizeof(person_details));
    printf("size of person: %ld byte\n", sizeof(struct person));
    return 0;
}
実行結果
name: ushijima, from: Saga, age: 28, tall: 174.8
size of pointer: 8 byte
size of person_details: 16 byte
size of person: 32 byte

ここで二個目のなんでやねんポイントです。
構造体は、中の要素の領域の和になるはず...にもかかわらず、person_details型は16バイトになっています。
これは、C言語のstructの特性で、64bitの場合、構造体全体は8の倍数にするように、空いた隙間を埋めるようになっています。
そうすることで、アクセスする速度が速くなります。
従って、16-12=4バイトのデータを入れることで、8の倍数になっています。

以前、私が触っていたときは、構造体の初期化の際に、0クリアを徹底するように言われました。
想定しない何かが起きた時、この隙間に入れたデータが予期せぬ挙動を起こすからです。

今の言語では、あまり意識しなくても書けると思いますが、データが実態かどうかを考えるきっかけにはなるかなと思います。


エラー処理

最後にエラー処理の話をします。
C言語で私が行ってきたエラー処理は以下のようなものです。

C
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 構造体の宣言(12byte + 4byte)
typedef struct {
	int num;    // 4 byte
	char *str;  // 8 byte
} struct_tmp;


int func1(struct_tmp *tmp) {
    int char_num_limit = 32;

	tmp->num = 0;

    // 32byteの領域の確保
	tmp->str = (char*)malloc(sizeof(char)*char_num_limit);
    memset(tmp->str, '\0', sizeof(char)*char_num_limit);
    
    char *s1 = "aaaaaaaaaa"; // 10byte
    //char *s1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; // 30byte
    char *s2 = "aaaaaaaaaa"; // 10byte
    int char_num = 0;
    // 文字数カウント
    for(int i = 0; s1[i]!='\0'; ++i, ++char_num);
    for(int i = 0; s2[i]!='\0'; ++i, ++char_num);
    tmp->num = char_num;

    // 文字列大きすぎれば処理を終了
    if (char_num > char_num_limit) {
        goto fatal_err;
    }

	sprintf(tmp->str, "str: %s%s", s1, s2);

    return 0;

fatal_err:
    // メモリの解放
	free(tmp->str);
	free(tmp);

    printf("fatal error\n");
    return -1;
}

int main() {
    // ポインタ変数
    struct_tmp *tmp = NULL;

    // struct_tmpの大きさ分の領域を確保(16byte)
    tmp = (struct_tmp*)malloc(sizeof(struct_tmp));
    memset(tmp, '\0', sizeof(struct_tmp));

    int ret = func1(tmp);
    printf("return: %d\n", ret);

    // 正常値が返ってきた場合
    if (!ret) {
        // 関数で処理した構造体のデータを出力
        printf("%d byte\n", tmp->num);
        printf("%s\n", tmp->str);
        
        // メモリの解放
        free(tmp->str);
        free(tmp);
    }

    return 0;
}
実行結果
return: 0
20 byte
str: aaaaaaaaaaaaaaaaaaaa
実行結果(エラー)
fatal error
return: -1

goto文というものがあり、ここで指定した場所の処理にジャンプするというコードです。
想像に難くない通り、諸悪の根源です。
今回のコードでは、エラーが起きたらgoto文を使ってfatal_errまで飛ばしています。
そこでメモリを開放し、printをしてreturnを返しています。
そうです。Cでは、return値というのはその処理が失敗したか成功したかを返すものでした。
エラーコードというものを管理し、処理が何か異常値を返す時、そのコードをreturnします。

それでは、中で処理した内容はどうやって関数の外に渡すのでしょうか。
答えは、引数です。
処理したいデータの容量分のメモリを先に確保し、そこに処理したデータを格納していきます。
最後に確保したメモリを解放し、一連の流れとなるわけです。
こんな感じで例外の処理やエラーを判断していました。大変ですね...

Rustではこんなことをしなくてもよいです。
便利なものがありますね。そうです、Option/Resultです。

Rust
fn result_test() -> Result<String, ()>{
    let mut n = 0;
    let mut st = "".to_string();

    st = "aaaaaaaaaaaaaaaaaaaa".to_string();
    //st = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string();
    n = st.len();
    if n > 20 {
        return Err(())
    } 
    Ok(st)
}

fn main() {
    println!("{}", result_test().unwrap());
}
実行結果
ussy@DESKTOP-91CH228:~/c$ ./result 
aaaaaaaaaaaaaaaaaaaa
実行結果(エラー)
ussy@DESKTOP-91CH228:~/c$ ./result 
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: ()', result.rs:15:34
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

簡単にエラー時の処理を分けつつ処理した値をreturnで受け取ることができます。
なんて便利なんでしょう!
しかも失敗したときは確定で落ちてくれます。
これが厄介なところで、メモリをいじりながら書いていると、実は落ちずに処理が続行されたりします...
確定でエラーになることは素晴らしいことだと思います。

まとめ

いかがでしたでしょうか。
様々な言語からRustに来た時に素晴らしい面が見れると思いますが、今回はCからそれ以外の言語を見た時に見える景色を紹介しました。
実際には、メリットも勿論あるのですが、今回は私がRustを触り始めた時はこんな風に見えていたことを案内しました。
皆さんはRustがどんな風に見えていますか?また、昔はどんな風に見えていましたか?

83
61
19

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
83
61