がるの健忘録

エンジニアでゲーマーで講師で占い師なおいちゃんのブログです。

C/C++のアドレス演算子、と、golang

ちょいと別所(身内のSlack)で「golangの参照渡し周りのお話」が出ていたので、その前提知識……が「長くなりそう」なのと「一部、もしかしたら役に立つかも」なの*1等々、ありまして、こちらに。
お話がgolangなので&おいちゃん、golangは「一定以上に評価しているし」「興味あるし」「がっつりやりたい」んだけど、まだ出来ていないので、まずは簡単なテストコードを書いてから。

package main

import (
        "fmt"
)

func hoge(n int, pn *int) {
        fmt.Printf("\nin hoge\n")
        fmt.Printf("\t n: %p\n", &n)
        fmt.Printf("\tpn: %p\n", pn)
}

func main() {
        var n int = 10
        var pn *int = &n

        //
        fmt.Printf("\t n: %p\n", &n)
        fmt.Printf("\tpn: %p\n", pn)
        //
        hoge(n, pn)
}

実行

[gallu@nnnnnnnnnn go]$ go run t1.go
n: 0xc000016088
pn: 0xc000016088

in hoge
n: 0xc0000160a8
pn: 0xc000016088

うん、予想通り「C/C++と一緒」だわ。

ってなわけで、比較的思う存分(笑

まず先に。
golangは詳しくないので「もしかしたらあるかもしれない」のですが、現状見ている限りだと「golangには参照渡しはない」と思われます。
あるいは「参照の値渡し」。

参照渡しは、例えば「C++」には存在していて。

#include <iostream>

// "参照"渡し
void hoge(int &i) {
    i++;
}

// "参照の値"渡し
void foo(int *i) {
    (*i) ++;
}

int main () {
    int i= 0;
    hoge(i);
    std::cout << i << std::endl;
    foo(&i);
    std::cout << i << std::endl;

    return 1;
}

hogeは「参照」そのものを渡しているので、callしている方も「なんも気にせずに変数を渡している」のに、なんでかいつの間にかそれが参照になっちゃってる摩訶不思議*2
一方で、fooは「ポインタ(参照)」のアドレスを「値として」渡して、関数のなかで「デリファレンス(間接参照)」しているので。callしているほうは「変数」じゃなくて「変数のポインタ」を意図的に渡す必要があるし、受け取ったほうもそれは「ポインタを受け取った」だけなので、中身をいじりたければ「デリファレンスしてあげる」必要がある。

いやまぁこの辺「いいじゃん面倒だし参照渡しって言っても」とか個人的には思ったりするのですが(それで弊害が起きた経験がないので)。
ただまぁ、細かくこの辺を気にされる御仁もいるので、一応軽い突っ込みというか前置きを。

で、前置いた上で。
golang、見ている限りだと「参照渡し」が見当たらないので、なんとなし「参照の値渡し」しかないのかなぁ? と思ってます。
まぁその辺は有っても無くても以降のお話にそんなに大きく影響しないのですが、一応。

んで、本題。
CでもC++でも、参照(の値)渡し(以降面倒なんで「参照渡し」に統一)をしたい時って

1. 値渡しにすることによってパフォーマンス影響があるか
2. その関数内で構造体の値を変更したいか

って辺りかなぁ、と、その辺はlocalのSlackで出ていた所と、そんなに変わらず。

まず「2番」について。
これ、本当に言語によって是非が大きく変わると思っていて。
Rubyが顕著なような気がしているのですが、この手の「引数の値を呼び先で変更する」のを「破壊的メソッド」って呼称しているようです*3
あちこちの文章を読んでいるに、基本的にどちらかというと「ほんのり否定的(注意しろ、警戒しろ)」な文脈が多いように見受けられます。

まぁ、おいちゃんの最近の主戦場はPHPですが、PHPでも引数に&を付ける事で参照渡し*4が出来ますが、基本的に「すんな」って言うことが多いから、まぁ、えぇ。
この辺は「プログラムの安全性」とか「デバッグ時の問題の見つけやすさ」とか、色々あります……そこが少し極みまでいくと「参照透過性」てな話しになってきます*5

一方で、例えば特にC言語*6の場合。
メモリとかメモリとかメモリとか、色々とありまして「わざわざreturnだけのために引数と同じメモリ領域を改めて確保とかしてられっかい」とか「その動的に確保したメモリ、どこでどうやって解放すんだよ?」とかあるので。
割合と
・呼び元で「戻り値を入れるメモリ空間」をあらかじめ確保
・引数に「戻り値を入れる変数へのポインタ」を渡す
・関数をcallして値を入れて貰う
・値を使う
・「戻り値を入れる」変数のメモリを解放
なんてのをちょいちょいとやってました……今は違うのかなぁ??
上述みたいなケースだと「その関数内で構造体の値を変更したい」ってか「変更しなきゃいかん」ので、そもそも変更が必須になるので「まぁポインタ渡すよねぇ」と。

さて、割と気になるのが1番。
先に主戦場(PHP)の話しをちょろっとしておくと。
PHPで"パフォーマンス目的"で参照渡し、は、ゼッタイにすんな」以上終了。パフォーマンスは「むしろ落ちる」ので……なんで落ちるかは、暇があったら書きます(昔どこかの原稿で書いた記憶があるんだが……)。
必要になる知識エリアとしては「copy-on-write(コピーオンライト) https://www.php.net/manual/ja/internals2.variables.intro.php 」あたり、になります……「記事書け」って意見があったら言っていただければ、改めて、書きます。

CとかC++の場合。
幾分状況次第ってのもあるんですが「パフォーマンス目的、は、割とある」と記憶をしています。
この辺でおいちゃんの記憶に鮮烈なのが、C++での事なのですが。

まぁ故あって(それ自体は仕方が無い)、割とでかいstringクラスのインスタンス、があって。
あちこちのメソッドに引数として渡されていって処理をして回っていたのですが。
それが、全体的に

std::string hoge(std::string s)

って感じの引数/戻り値で構成されていて……端的にはご依頼が「いわゆるC++で組まれたCGIなんだが、アクセス数が多いとメモリが足りなくなってサーバが重くなったり落ちたりする」というご依頼で……さもアリナミン
全体的に

void hoge(std::string& s)

って書き直したら割とあっさりと落ち着きましたよ、というのが、多分、一番この手の話し的には「印象の強い」お話かなぁ、と。

そりゃそうだよ関数呼ぶたびにがっつんがっつん、インスタンスcopyしてるんだもん。しかも「呼ばれた関数の中で別の関数を呼ぶ」とかってネストで出来てたから、そりゃたっぷり山盛り特盛りつゆだくだくにメモリ食いますってば。
この手のシチュエーションの場合、確実に「パフォーマンス(もっと具体的には"メモリを無駄食いさせない"という意図)」目的で、参照渡しをチョイスします。

ちな一瞬余談。
「パフォーマンスのために参照渡ししているけど中身は変えて欲しくない」場合、C++だと

void hoge(const std::string& s)

とかやって「変えるなよ?」と釘をさせるのが便利でねぇ……PHPにこの機能入らないかしらん? 割とマジで。

閑話休題

本題のgolang
大体C/C++と同じように動くようなので。特にでかい構造体とかを「そのまま渡す」とがっつりcopyが走りそうなので、それを考えると「チューニング目的としての参照渡し」は、golangはそこそこ「タイトな要求/環境で動かしたい」言語だと思われるので、普通に「あり」なんじゃないかなぁ、と思われます。
C++の引数にconst」相当の機能は……今、斜めに見た限りだと、golangにはなさそうだなぁ……この辺は紳士協定なんかし?

んで、パフォーマンスを前提にした場合。
例えば「intの変数を参照渡し」しても、うまみはないです。理由はわかるな?
charとかもっと無意味だからな? わかるな?
最低でも「ポインタで扱われている型、以上のサイズを持つ変数」に対して行うようにしましょう。……まぁ「構造体/クラスインスタンスの時にやる」くらいでいいんじゃないかなぁ? 判断基準としては。

まぁそんなこんながあるので。
要約すると「パフォーマンス目的での参照渡し」は、言語によって異なるけど「golangならあると思われる」でございます。


あと、ついでに1つ。
この辺の記事書くのに調べてて面白いと思ったのが「golangではスタックとヒープを気にする必要が無い https://qiita.com/rookx/items/a1e3d057a0ed71424094 」んだそうでございます。
へぇ。

端的には
・関数内で普通に宣言された変数が置かれるスタック領域:メモリの確保と解放が楽なんだけど使用制限あり
・動的なメモリ確保とかで置かれるヒープ領域:自由に使えるけどちょっとだけ(内部構造含め)面倒
ってのがあって。

C/C++でわかりやすいのが、これ。

#include <stdio.h>

int *hoge() {
    int i = 100;
    return &i;
}

int main() {
    int *pi = hoge();
    return 0;
}

[gallu@nnnnnnnnnn go]$ gcc t2.c
t2.c: 関数 ‘hoge’ 内:
t2.c:5:5: 警告: 関数が局所変数のアドレスを返します [-Wreturn-local-addr]
return &i;
^
[gallu@nnnnnnnnnn go]$

なんでかってぇと
hogeの中の変数iはスコープが「hogeの関数内」なので、関数を出たら「どうなるかわからない(普通に考えて、大いなる混沌に戻される)」
・のに、それのポインタ返すとか「あんたバカァ?」@惣流・アスカ・ラングレー

なんだけど*7。どうもgolangは「これが出来る」らしい。
コードは前述のブログにあるので、略。

まぁこの辺が出来ると
・呼び出された関数の側で(がっちょりした)構造体とか作って
・戻り値として、(チューニング目的もあるから)「構造体のポインタ」を返す
ってコードが「言語として保証されたレベルで」普通に書けるので、便利は便利だと思うんだよねぇC/C++から来た人は、しばらくモニョるような気がするんだけど。

とりま、やりたいやりたい思ってたgolangを、ちょろっとでも調べられるよい機会だったので、割とこりこりと調べて書いてみましたよ、っと。
とはいえ、C/C++も幾分久しい(のと実務の経験としては古い)、golangはド素人同然、なので。突っ込みどころがあったら、優しく突っ込んでいただければ幸いでございます。

*1:とBlogの良いネタになるw

*2:別に不思議じゃない

*3:本質的に「どの言語でもある」はずですが、ググると、破壊的メソッドの上位にはRubyの話題が多いんですよねぇ

*4:PHPは本当に参照渡し

*5:個人的には「ある程度、わかる」

*6:とはいえ、おいちゃんがお仕事でCをガリゴリ書いてたのっていい加減15~20年くらい前なので、色々と加減算してくださいませ

*7:この辺が「変数iがスタック領域に存在しているあたりだったりもするんだけど、説明は省略