文字列と配列を対象にC++/Python/Juliaでこれらの違いをメモる.

C++

コピー

#define PRINT_ADDR(arg){                                    \
    cout << "addr of " << #arg << " is " << &arg << endl;   \
  }

int main(){
  string str1 = "A string";
  string str2 = str1;

  vector<int> vec1 = {1, 2, 3, 4};
  vector<int> vec2 = vec1;

  PRINT_ADDR(str1);
  PRINT_ADDR(str2);
  PRINT_ADDR(vec1);
  PRINT_ADDR(vec2);
}

/*
addr of str1 is 0x7fff4d146040
addr of str2 is 0x7fff4d146060
addr of vec1 is 0x7fff4d146000
addr of vec2 is 0x7fff4d146020
*/

普通に代入演算子を用いるとオブジェクト領域に文字列や配列の内容はコピーされる.よってそれぞれのオブジェクトのアドレスは全て異なる.

参照

参照するとアドレスの指す先は同じになる.

#define PRINT_ADDR(arg){                                    \
    cout << "addr of " << #arg << " is " << &arg << endl;   \
  }

int main(){
  string str1 = "A string";
  string &str2 = str1;

  vector<int> vec1 = {1, 2, 3, 4};
  vector<int> &vec2 = vec1;

  PRINT_ADDR(str1);
  PRINT_ADDR(str2);
  PRINT_ADDR(vec1);
  PRINT_ADDR(vec2);
}

/*
addr of str1 is 0x7ffd19402590
addr of str2 is 0x7ffd19402590
addr of vec1 is 0x7ffd19402570
addr of vec2 is 0x7ffd19402570
*/

string_view

C++の文字列は

string str = "hello world";

すると実行時にメモリを確保(@ヒープ)する.一方string_view

string_view sv = "hello world";

すると,文字列"hello world"自体は@静的領域に確保され(つまりコンパイル時にELFのデータとして配置される),svはこの文字列の先頭へのポインタと文字列の長さを持つ.よって,そもそも静的領域であるから当然だが,string_viewread-only である.string_viewのメンバ関数は,この先頭へのポインタと文字列の長さを変更することでstringと似たような操作を実現している.

例1

string_view sv = "hello world";
string_view sub = sv.substr(1, 6);
cout << sub << endl;

/*
ello w
*/

subは"ello w"を取り出して別の領域にコピーしたのではなく,先頭のポインタをeに,文字列の長さを6に変更しただけ.

例2

string_view sv = "   hello world";
sv.remove_prefix(min(sv.find_first_not_of(" "), sv.size()));
cout << sv << endl;

/*
hello world
*/

これはsvの先頭が指すポインタを" “からhに移動しているだけである.よって実際には静的領域に確保された” hello world"の先頭の空白を 削除していない

Python

コピーと参照

Pythonでは

a = b
b = c

するとa, b, cすべてが同じメモリ領域を指す.異なる領域に変数のコピー(スナップショット)を作りたい場合,copy.deepcopy(x)などを使う.copy.deepcopy(x)は複合オブジェクトのメンバ変数などを再帰的に全てコピーするが,copy.copy(x)は複合オブジェクトのメンバ変数の参照をコピーする.

numpyのview()

特に参照と変わらない.

Julia

コピーと参照

JuliaもPythonと同様に変数に代入演算子を用いると参照がコピーされる.配列のスナップショットを別に作りたい場合はcopy(x)を使う.

a = ones(3);
b = a;
c = copy(a);

a == b // true
a === b // treu
a == c // true
a === c // false

==は値の一致を,===はオブジェクトとしての一致を含めてチェックする.

ビュー

Juliaの配列へのビューは,参照の配列のような側面があり結構面白い.

> a = reshape(collect(1 : 16), 4, 4)
4×4 Array{Int64,2}:
1  5   9  13
2  6  10  14
3  7  11  15
4  8  12  16
> b = view(a, :, 3);
4-element view(::Array{Int64,2}, :, 3) with eltype Int64:
9
10
11
12

このbaの3列目への参照の配列である.よってbを介してaを変更できる.

> for i = 1:4
     b[i] *= 10
  end
> a
4×4 Array{Int64,2}:
1  5  90  13
2  6  100  14
3  7  110  15
4  8  120  16

bを介してaの3列目を10倍した.

JuliaのviewはPythonよりもさらに柔軟性があり,スライスのようなこともできる.

> a = reshape(collect(1:16), 4, 4);
> c = @view a[2:end, 2:end]
3×3 view(::Array{Int64,2}, 2:4, 2:4) with eltype Int64:
6  10  14
7  11  15
8  12  16

これも同様にcを介してaの一部を10倍してみる.

> for i = 1:9
    c[i] *= 10
  end
> a
4×4 Array{Int64,2}:
1   5    9   13
2  60  100  140
3  70  110  150
4  80  120  160