8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

(C++における)関数の参照渡し、参照型の値渡し、ポインタ型の値渡し

Last updated at Posted at 2017-12-05

この記事を書くきっかけ。

C++は値型と参照型、ポインター型を持ちそれぞれに値渡し、参照渡しができる。そのことについて(初心者向け) JavaScript の関数 (ES6対応)関数パラメータの参照渡しとはどういうことなのか?で主にポインタ型の値渡しについて盛大に勘違いしていたので自分の頭の整理がてら記事にまとめる。
参照渡し(値型の参照渡し)、と参照型の値渡しについては他の言語にもにもありさんざん議論になっているが殆どの言語でC++と同じ動作をすると思う。
C言語ではポインタ型と値型しかなく、関数は値渡ししか出来なので値の値渡しとポインタ型の値渡しをポインタ渡しと呼べるが、本記事は値渡しと参照渡しがあるC++について語るのでポインタ型の値渡しを使う。
C++の参照型の参照渡しは型推論の規則により参照渡しに解決されるだろうが私の理解を超えるので本記事では深く解説しない。
ポインター型の参照渡しはC++では基本的に参照を使うべきの立場でその指針で使ってきたので語れるだけ含蓄がないのでこちらも解説しない。

参照渡しと値渡し

情報処理試験問題に合致するものを参照渡しと値渡しとする。

参照渡し
#include <iostream>

void SubX(int &C, int D){
  int E;
  E=C;
  C=D;
  D=E;
}

int main(){
  int A, B;
  A=1;
  B=2;
  SubX(A, B);
  std::cout<<"A="<<A<<"  B="<<B<<std::endl; // A=2  B=2
}
参照型の値渡し
#include <iostream>

void SubX(int C, int D){
  int E;
  E=C;
  C=D;
  D=E;
}

int main(){
  int A, B;
  A=1;
  B=2;
  SubX(static_cast<int&>(A), B); // 
  std::cout<<"A="<<A<<"  B="<<B<<std::endl; // A=1  B=2
}

ここではstatic_castでint型からint&への型変換を行っている関数の呼び出し部分での型変換でなくても関数呼び出し前で参照型として確保しても同じことである。結果が異なるので異なるものであり。一般に言われる参照渡しと同じ動きをするのは当然、参照渡しである。

call of overloaded ‘SubX(int&, int)’ is ambiguous

C++を使っているとよく出会うエラーの一つで'何か'の定義が曖昧で(ambiguous)あるの意味である。

曖昧な定義
#include <iostream>

void SubX(int C, int D){
  int E;
  E=C;
  C=D;
  D=E;
}

void SubX(int& C, int D){
  int E;
  E=C;
  C=D;
  D=E;
}

int main(){
 // 省略
}

C++では関数はオーバーロード可能ではあるが似た型を定義してしまうと以下のように型解決ができずにambiguous errorをだす。

ポインタ型の値渡し関数はオーバーロード可能

参照渡しとポインタの値渡し
#include <iostream>

void SubX(int& C, int D){
  std::cout<<"Call SubX(int&, int)"<<std::endl;
  int E;
  E=C;
  C=D;
  D=E;

}

void SubX(int* C, int D){
  std::cout<<"Call SubX(int*, int)"<<std::endl;
  int E;
  E=*C;
  *C=D;
  D=E;
}

int main(){
  int A, B;
  A=1;
  B=2;
  // Call SubX(int&, int)
  SubX(A, B);
  std::cout<<"A="<<A<<"  B="<<B<<std::endl; // A=2 B=2  
  // Call SubX(int*, int)
  SubX(&A, B);
  A=1; 
  /** SubX(static_cast<int*>(A), B);  **/ // 型変換エラー
  std::cout<<"A="<<A<<"  B="<<B<<std::endl; // A=2 B=2                                                 
}

ポインタ型(int*)と値型(int)は(暗黙の)型変換できない、&intの部分はグローバル演算子&(値型からその値型を指すポインタ型を返す演算子)でポインタ型へ変換している。
暗黙の型変換がないので型の解決の時点で曖昧になることはないおかげでf(int)とf(int*)もしくはf(int&)とf(int*)`はオーバーロード可能である。

ポインタは値渡しである

ポインタ型の値渡しであることの確認
#include <stdio.h>                                                                                                                          
#include <iostream>

void SubX(int* C, int D){
  printf("address of C : %p\n", &C);                                                                                                       
  std::cout<<"address of C : "<<&C<<std::endl;
  int E;
  E=*C;
  *C=D;
  D=E;
}

int main(){
  int A, B;
  A=1;
  B=2;
  int* ptr_A=&A;
  printf("address of A : %p\n", &ptr_A); 
  std::cout<<"address of A : "<<&ptr_A<<std::endl; // 
  SubX(ptr_A, B);
  // printf("A=%d  B=%d\n", A, B);  
  std::cout<<"A="<<A<<"  B="<<B<<std::endl; // A=2  B=2
}

ポインタ型は指している変数のアドレスを値として持っているのでその値が入っているアドレスというのができる。だからポインタ型のアドレス($\neq$ポインタ型自体の値)が存在する。それぞれを逐一確認していくと

ポインタ型の値渡しであることの確認
#include <stdio.h>
#include <iostream>

void SubX(int* C, int D){
  printf("address of C : %p\n", &C);
  std::cout<<"address of C : "<<&C<<std::endl;
  int E;
  E=*C;
  *C=D;
  D=E;
}

int main(){
  int A, B;
  A=1;
  B=2;
  printf("address of A : %p\n", &A);
  std::cout<<"address of A : "<<&A<<std::endl;
  int* ptr_A=&A;
  printf("address of &A : %p\n", &ptr_A);
  std::cout<<"address of &A : "<<&ptr_A<<std::endl;
  SubX(ptr_A, B);
  std::cout<<"A="<<A<<"  B="<<B<<std::endl;
}
結果
address of A : 0x7fff1f493d4c
address of A : 0x7fff1f493d4c
address of &A : 0x7fff1f493d50
address of &A : 0x7fff1f493d50

##
address of C : 0x7fff1f493d18
address of C : 0x7fff1f493d18
A=2  B=2

値型Aのアドレスと、Aのアドレスを代入したポインタ型のptr_AとSubX内でのポインタ型のCではそれぞれアドレスは違う。(もちろんアドレスの値自体は環境依存だがポインタ型で関数に渡す以上、ptr_AとCは別の(別のアドレスに確保される)変数になる。)
つまりptr_AとCは別の変数として別のアドレスに確保されている。
では何故、ポインタで参照渡しができるかというとポインタ型からアドレス先の値(の参照)を返す*演算子(間接演算子)があるからである。
これを使った代入処理は何が問題かというと間違えれるからです。これぐらいだと間違える可能せは低いかもしれないが複雑な関数だと間違える可能性が上がるし、手段として間違えれる方法が残されている以上間違える可能性は0でない(実際、私は間違えた)。
なのでC++では出来る限りポインタを使わないことを推奨している。

参照とは何か?

(値型の)参照渡し

参照の検証
#include <stdio.h>
#include <iostream>

void SubX(int& C, int D){
  printf("address of C : %p\n", &C);
  std::cout<<"address of C : "<<&C<<std::endl;
  int E;
  E=C;
  C=D;
  D=E;
}

int main(){
  int A, B;
  A=1;
  B=2;
  printf("address of A : %p\n", &A);
  std::cout<<"address of A : "<<&A<<std::endl;
  int& ref_A=A;
  printf("address of reference A : %p\n", &ref_A);
  std::cout<<"address of reference A : "<<&ref_A<<std::endl;
  SubX(A, B);
  std::cout<<"A="<<A<<"  B="<<B<<std::endl;
}
結果
address of A : 0x7ffe6bda0ca4
address of A : 0x7ffe6bda0ca4
address of reference A : 0x7ffe6bda0ca4
address of reference A : 0x7ffe6bda0ca4
address of C : 0x7ffe6bda0ca4
address of C : 0x7ffe6bda0ca4
A=2  B=2

参照は別名であるとも言われる通り、値型とその変数の参照型は常に同じアドレスを指しているし参照渡ししても当然アドレスが変わることはない。別名なので暗黙の型変換がはたらくのも当然である。
今回は変数からそれを指すポインタを返す&(アドレス)演算子を使ったが参照はプログラマーがローレベルにアクセスする必要がないものをに対してアドレスを隠匿して値に対してしかアクセスできるようにしたものなので&による参照のアドレスへのアクセスはするべきではない。

参照型の値渡し

参照型の値渡し
#include <stdio.h>
#include <iostream>

void SubX(int C, int D){
  printf("address of C : %p\n", &C);
  std::cout<<"address of C : "<<&C<<std::endl;
  int E;
  E=C;
  *(&C)=D;
  D=E;
}

int main(){
  int A, B;
  A=1;
  B=2;
  printf("address of A : %p\n", &A);
  std::cout<<"address of A : "<<&A<<std::endl;
  int& ref_A=A;
  printf("address of reference A : %p\n", &ref_A);
  std::cout<<"address of reference A : "<<&ref_A<<std::endl;
  SubX(ref_A, B);
  std::cout<<"A="<<A<<"  B="<<B<<std::endl;
}
結果
address of A : 0x7ffce88b6b24
address of A : 0x7ffce88b6b24
address of reference A : 0x7ffce88b6b24
address of reference A : 0x7ffce88b6b24
address of C : 0x7ffce88b6afc
address of C : 0x7ffce88b6afc
A=1  B=2

値渡しなので変数自体を別のアドレスにコピーを作成する。別の変数なのでポインタに変換して関節参照演算子で値を書き換えても関数の外に影響しないので**(値型の)参照渡しと同じことはできない**。

まとめ

C++で値型、参照型、ポインタ型、値渡し、参照渡しの文脈で考えると以下の6個があるが今回は4個を解説したC++では参照型の参照渡しは参照渡しに型変換を使って解決されるはずだが正確な解説にはC++の型推論が必要なので私のC++力では正確な解説はできないので省略する。
値型の参照渡しとポインタ型の参照渡しがあるので値型の参照渡しだけを参照渡しと呼ぶのは不公平だというかもしれないがC++では生ポインタを使うならスマートポインタを使うべきとしているスマートポインタもclassなので受け渡しは参照でされるべきであり原状のC++でポインタ型の参照渡しを考慮する必要性はないと思う。
ポインタ渡しという言い方があるがそれは値渡ししかないC言語で値型とポインタ型しかないでの文脈なら良いかもしれないが参照渡しの存在するC++では適当でないと考える。またポインタ型の参照渡しがないならポインタ渡しでいいというかもしれないが値渡し、参照渡しの文脈で考えるなら値渡しだし他の値渡しの省略を認めていないのでポインタ渡しという言い方は適当でないと考える。同じくC言語での解説でもポインタ型が参照型に間違うような解説は避けるべきだと考える。
表にまとめるとこんな関係性である。

値型 参照型 ポインタ型
値渡し 値型の値渡し 参照型の値渡し ポインタ型の値渡し
参照渡し 参照渡し (参照渡し) C++非推奨

あとがき

C++を安全に取り扱う方法

ポインタ型は無効なアドレスであるNULL(C++11以上ではnullptr)を指せる。NULLへのアクセスは未定義動作を引き起こし危険なのでNULLを指せないより安全な参照を使うべきである。
それ以外に継承などでアドレスに直接アクセスする必要がある場合でもスマートポインタを使うべきである。
参照渡しは便利である一方、関数を抜けた時点で引数に渡した変数が書き換えられている可能性がある。これは関数の動作を不安定にするのでconst修飾された参照渡しすることを推奨している。

その他の言語の状況

C#での値型、参照型、ポインタ型

C#歴は3日であるw。調べてみるとC#には値型参照型も、unsafeなので推奨されていないがポインタ型も存在する。
main関数はC/C++と同じように書けないものの値渡し、参照渡し、ポインタ渡しはC/C++と同じ挙動をしたので一応、この記事の議論は成り立つと思う。

JavaScriptの例

元記事ではJavaScriptが議論の発端だったのでJavaScriptについて考察する。
JavaScriptはすべての変数がオブジェクトだと言われている。そしてMDNにはObjectは参照型であるとある(オブジェクトの比較)。そして関数の引数が参照渡しであるとの記述はMDNの中には見つけられなかった。更に、オブジェクト(配列)であってもC++の参照型の値渡しと同じ動作をする。

nodeで実行
'use strict'

const subX=(c, d)=>{
    const e=c;
    c = d;
    d = e;
}

const a= 1;
const b= 2;

subX(Number(a), b); 

console.log(a, b); // 1, 2

なので他言語のことを考えて記述するなら参照型の値渡しが正しいと思う。
C言語だけの記述だとポインタを参照と読んでいたりするがそれは他言語(C++)を考えると正しい記述ではないということは上で述べた。ポインタ型の値渡しで参照渡しと同等のことができるのはポインタのアドレスにアクセスできる間接演算子があるためである。JavaScriptにはこの間接演算子に相当する機能がないのでJavaScriptは参照渡しであるとか参照渡しと同等のことができるとかの記述は間違いであると考える。

多くの言語に対する一般論

ほぼすべての参照型を使う言語で間接演算子のような参照している変数に直接アクセスするような機能を提供している例は見たことがない。それは参照がアドレスなどのローレベルを隠匿してより安全に使おうという思想に反しているからである。
間接演算子を提供するなら参照渡しだけを提供したほうが安全なので参照渡しを提供している言語はいくつかある。

何故、参照型の値渡しが参照渡しとなってしまったのか?

これは参照の概念のないC言語ではポインタ型の値渡しをポインタ渡しと言ったりポインタを参照と言ったりした説明が多用されたためだと思う。それに、C++ができた時にポインタ型の値渡しが参照渡しと同等なことができることからポインタ=参照、ポインタ渡し=参照渡しとなってしまったためだと推察する。
しかしポインタ渡し(ポインタ型の値渡し)でもポインタの指す先にアクセスする手段がなければ参照渡しと同等のことはできずに参照の値渡しと同等のことしかできない。(できることといえば参照で扱えないNULLへのアクセスだがその機能は危険である)
C++といえば古い言語のように思えるが標準が出来上がったのが1998年(C++98)であるのでそこまで古い言語でもない。むしろPythonのバージョン2のリリースが2000年10月1995年登場、1997年標準化のJavaScript1995年に登場したJava等とほぼ同時期の言語と言えるのでその前身であるC言語からの説明が多いのも仕方ないかもしれないが参照渡しという概念自体は更に古いfortranの時からある概念でこちらに則ったC++の参照に従った説明がなされるべきだと思う。
特に昨今は複数の言語を扱うことが必須条件なので参照渡しと参照型の値渡し、更にはポインタ型と参照型の違いを区別して、その上で言語が提供するインターフェイスを意識して記事を書けるようになりたいし書けるようになってほしい。

元記事での間違い

私が元記事で間違ってしまったのはいきなり低レベル(アドレスとか)に話を飛ばしてしまったことである。元記事@mpyw さんのコメントにあるように(C言語含む)高級言語ではその言語が見せているインターフェイス上で話すべきで低レベルを見る前にその言語のみで確かめられることを確認することだと思い知りました。

最後に

似て(非な)るものにこそ気をつけなければならないと感じました。似ているものを同一視してしまうとそこにズレが起きますそのズレを表面化させた時、エラーが発生します、表面化しなくてもそれは(潜在的な)バグだと思います。

8
5
0

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
8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?