forked from dlang/dlang.org
-
Notifications
You must be signed in to change notification settings - Fork 4
/
hijack.dd
571 lines (448 loc) · 16.3 KB
/
hijack.dd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
Ddoc
$(D_S 関数ハイジャック軽減法,
$(P
ソフトウェアが複雑化するにつれ、我々は、より一層モジュールのインターフェイスに
依存するようになっています。アプリケーションは、複数のソースからなる複数のモジュール、
社外のソースを含むモジュールなども組み合わせて使っています。
各モジュールの開発者は、一緒に使われる他のモジュールについて踏み込んだりする必要無しに、
自分のモジュールの開発を続けることが可能でなければなりません。
そしてアプリケーションの開発者は、モジュール側の変更がアプリケーションに破壊的な影響を与えるならば、
その変更に気づけるようになっていることが重要です。
このドキュメントでは、モジュールにおける無害な問題のない宣言の追加が、
C++やJavaのアプリケーションプログラムにありとあらゆる大惨事を引き起こす
「関数ハイジャック」についてお話しします。
そして、D言語がどんな言語設計の変更で
この問題を大幅に軽減しているのかについても
見ていこうと思います。
)
$(SECTION2 グローバル関数ハイジャック,
$(P 二つのモジュール ‐ XXX社のXモジュールとYYY社のYモジュール ‐ を
import するアプリケーションの開発中としましょう。
モジュール X と Y はお互い無関係で、
完全に違った目的に使用されます。
こんな感じの関数を提供しています:
)
----
module X;
void foo();
void foo(long);
----
----
module Y;
void bar();
----
$(P アプリケーション側のプログラムはこうなります:
)
----
import X;
import Y;
void abc()
{
foo(1); // X.foo(long) を呼ぶ
}
void def()
{
bar(); // Y.bar(); を呼ぶ
}
----
$(P ここまでのところは問題ありません。アプリケーションはテストされ無事動作し、出荷されました。
時は流れ、プログラマも入れ替わり、このアプリケーションはメンテナンスモードに移ります。
一方その頃、YYY社は顧客からの要望に応え、
モジュールに 型 $(CODE A) と関数 $(CODE foo(A)) を追加しました:
)
----
module Y;
void bar();
class A;
void foo(A);
----
$(P メンテナンスプログラマはYの最新版を手に入れ、
再コンパイルします。まだ問題はありません。
しかしここで、YYY社が $(CODE foo(A)) の機能を拡張し、
関数 $(CODE foo(int)) を追加しました:
)
----
module Y;
void bar();
class A;
void foo(A);
void foo(int);
----
$(P さて、我らがメンテナンスプログラマがいつものようにYを最新版に入れ替え再コンパイルすると、
突然アプリケーションの挙動がおかしくなりました:
)
----
import X;
import Y;
void abc()
{
foo(1); // X.foo(long) ではなく Y.foo(int) を呼ぶ
}
void def()
{
bar(); // Y.bar(); を呼ぶ
}
----
$(P $(CODE Y.foo(int)) の方が $(CODE X.foo(long))よりもオーバーロードのマッチとして適合度が高いためです。
しかし、$(CODE X.foo) の行うはずだった処理は
$(CODE Y.foo) とは完全に違うものなので、このアプリケーションは重大なバグを潜在的に抱え込むことになってしまいました。
最悪なのは、コンパイラからはこのような事態になっていることを示唆することがない/できない、ということです。
少なくともC++では、この動作が言語仕様の通りなのですから。
)
$(P C++ では、モジュール X や Y の中で名前空間や
ユニーク(と期待される)
接頭辞
を使った回避策が知られています。これはしかし、X や Y に対して手を加えることのできない
アプリケーションプログラマには役に立ちません。
)
$(P プログラミング言語 D でこの問題の解決のためにとった最初の方法は、
以下のルールを追加することでした:
)
$(OL
$(LI デフォルトでは、同じモジュールの関数どうしのみが
オーバーロードされる)
$(LI 同名の関数が複数のモジュールに存在する場合は、呼び出しの際には
完全修飾名を使わなければならない)
$(LI 複数のモジュールの関数をオーバーロードするには、
alias文を使ったオーバーロードのマージが必要)
)
$(P この規則があるため、YYY社が $(CODE foo(int)) の宣言を追加したときには
アプリケーションメンテナは
コンパイルエラーを目にすることになり(foo がモジュール X と Y の
両方で定義されているため)、問題に対処する機会が与えられます。
)
$(P これは一応の解決策にはなっていますが、制限が少しきつすぎます。例えば、
$(CODE foo(A)) が $(CODE foo()) や $(CODE foo(long)) と
混同されることはありえないのに、何故コンパイラがこれに文句を言うのでしょう?
結局のところ、
新たな解決策として「オーバーロード集合」の概念を持ち込むことになりました。
)
$(SECTION3 オーバーロード集合,
$(P 同じスコープで宣言された同名の関数グループが、
オーバーロード集合
を形成します。モジュール X の例では、関数 $(CODE X.foo()) と
$(CODE X.foo(long)) が1個のオーバーロード集合を形成します。
そして、関数 $(CODE Y.foo(A)) と $(CODE Y.foo(int))
もまた別のオーバーロード集合になります。fooの呼び出しを解決するステップは、以下のようになります:
)
$(OL
$(LI それぞれのオーバーロード集合ごとに別々に、オーバーロードの解決を行う)
$(LI どのオーバーロード集合でもマッチがなければ、エラー)
$(LI 1つのオーバーロード集合だけでマッチがあれば、それを選択)
$(LI 2つ以上のオーバーロード集合でマッチしていれば、エラー)
)
$(P この規則でもっとも重要な点は、あるオーバーロード集合でのマッチが他のオーバーロード集合のマッチより
より"適合度が高い"場合でも、依然としてエラーになるということです。
1つの関数が複数のオーバーロード集合に同時に含まれることはありません。
)
$(P 先ほどの例を使うと:
)
----
void abc()
{
foo(1); // Y.foo(int) には完全マッチ、X.foo(long) は暗黙変換を通したマッチ
}
----
$(P はエラーになりますが:
)
----
void abc()
{
A a;
foo(a); // Y.foo(A) に完全マッチ。Xからはマッチなし
foo(); // X.foo() に完全マッチ。Y からはマッチなし
}
----
$(P これは直感的に期待するとおりに、エラー無しでコンパイルされます。
)
$(P X と Y の $(CODE foo) をオーバーロードしたい場合は、次のようなコードになります:
)
----
import X;
import Y;
alias X.foo foo;
alias Y.foo foo;
void abc()
{
foo(1); // X.foo(long) ではなく Y.foo(int) を呼ぶ
}
----
$(P これはエラーにはなりません。先ほどのエラー例との違いは、
この場合はアプリケーションプログラマがXとYのオーバーロード集合を意図的に合成しているので、
何が起こるかをきちんと把握しており、XとYの更新時に $(CODE foo)
に変更がないかチェックする意志があると仮定できるということです。
)
)
)
$(SECTION2 派生クラスメンバ関数ハイジャック,
$(P 関数ハイジャックは他にもパターンがあります。AAA社のクラス $(CODE A)
があったとしましょう:
)
----
module M;
class A { }
----
$(P アプリケーションコードでは $(CODE A) から派生し、
仮想メンバ関数 $(CODE foo) を追加しています:
)
----
import M;
class B : A
{
void foo(long);
}
void abc(B b)
{
b.foo(1); // B.foo(long) を呼ぶ
}
----
$(P 万事OKです。しかしまた月日は流れ、AAA社
(もちろん $(CODE B) のことなど知りません) が $(CODE A) の機能をちょっと拡張するために
$(CODE foo(int)) 関数を追加しました:
)
----
module M;
class A
{
void foo(int);
}
----
$(P Java風のオーバーロード規則が採用されていたとすると、
基底クラスの関数と派生クラスの関数はお互いをオーバーロードします。
その結果、アプリケーション側の呼び出し:
)
----
import M;
class B : A
{
void foo(long);
}
void abc(B b)
{
b.foo(1); // A.foo(int) を呼ぶ!!!!!!!!!!
}
----
$(P で $(CODE B.foo(long)) を呼んでいたはずの部分が基底クラス $(CODE A) の
$(CODE A.foo(int)) にハイジャックされます。
そしてこの関数はおそらく $(CODE B.foo(long)) とは違った処理をする関数でしょう。
これが、私がJavaのオーバーロード規則を良しとしない理由です。
C++ はこの点では正しく考えられていて、派生クラスでは、同名の基底クラスの関数を
(例え基底クラスの関数の方がよりよいマッチであっても) マッチ候補としないようになっています。
D もこの規則に従います。
グローバル関数の場合と同様、両方の関数を混ぜてオーバーロードしたい場合は、
C++ではusingを使うのと同じように
D では alias 宣言で実現できます。
)
)
$(SECTION2 基底クラスメンバ関数ハイジャック,
$(P まだこれだけじゃないだろう、と思っておられる読者の方、正解です。
逆方向のハイジャックもありるのです。
派生クラスが基底クラスのメンバ関数をハイジャックするパターンです!
)
$(P こんなクラスを考えます:
)
----
module M;
class A
{
void def() { }
}
----
$(P アプリケーションコードでは、$(CODE A) から派生し
仮想メンバ関数 $(CODE foo) を追加しています:
)
----
import M;
class B : A
{
void foo(long);
}
void abc(B b)
{
b.def(); // A.def() を呼ぶ
}
----
$(P AAA社は、またしても $(CODE B) について何も知らず、
関数
$(CODE foo(long)) を追加して何か
$(CODE A) の新しい機能を実装するのに使用しました:
)
----
module M;
class A
{
void foo(long);
void def()
{
foo(1L); // A.foo(long) を呼ぶと期待されている
}
}
----
$(P しかし、なんと困った、$(CODE A.def()) は $(CODE B.foo(long)) を呼び出してしまうのです。
つまり $(CODE B.foo(long)) が
$(CODE A.foo(long)) をハイジャックしたわけです。
Aの設計者はこれを予見して
$(CODE foo(long)) を非仮想関数にしておくべきだった、と考える方もいらっしゃるでしょう。
しかし問題なのは、$(CODE A) の設計者が
$(CODE A.foo(long)) を仮想関数として使うことを意図して機能追加することも大いにあり得るということです。
彼にはすでに $(CODE B.foo(long)) が別の用途で存在することを知る術はありません。
論理的な帰結として、
このオーバーライドのシステムの中で安全に $(CODE A) に機能追加する方法はない、ということになります。
)
$(P D での解決策は簡単です。派生クラスの関数が基底クラスの関数をオーバーライドする際には、
必ず override 宣言を行わなければなりません。
override と宣言せずにオーバーライドを行おうとすると、
エラーになります。また逆に、何もオーバーライドしていない関数を
overrideと宣言するのもエラーです。
)
----
class C
{
void foo();
void bar();
}
class D : C
{
override void foo(); // ok
void bar(); // エラー。C.bar() をオーバーライドしている
override void abc(); // エラー。C.abc() は存在しない
}
----
$(P これによって、派生クラスのメンバ関数が
基底クラスのメンバ関数をハイジャックする潜在的な危険は取り除かれます。
)
)
$(SECTION2 派生クラスメンバ関数ハイジャック #2,
$(P 最後にもう一つ、基底クラスのメンバ関数が派生クラスのメンバ関数をハイジャックする
パターンがあります。
)
----
module A;
class A
{
void def()
{
foo(1);
}
void foo(long);
}
----
$(P $(CODE foo(long))
は何か特定の機能を持った仮想関数とします。
派生クラスの設計者は $(CODE foo(long)) をオーバーライドして、
派生クラスの目的に合わせてカスタマイズしようとします:
)
----
import A;
class B : A
{
override void foo(long);
}
void abc(B b)
{
b.def(); // 中で B.foo(long) が呼ばれる
}
----
$(P ここまでは問題ありません。$(CODE A) の内部の $(CODE foo(1))
は正しく
$(CODE B.foo(long)) を呼び出します。さて、$(CODE A) の設計者が最適化のために
$(CODE foo):
のオーバーロードを追加しました:
)
----
module A;
class A
{
void def()
{
foo(1);
}
void foo(long);
void foo(int);
}
----
$(P この結果、
)
----
import A;
class B : A
{
override void foo(long);
}
void abc(B b)
{
b.def(); // 中で A.foo(int) が呼ばれる
}
----
$(P おっと! $(CODE B) は $(CODE A) の
$(CODE foo) の機能を置き換えたつもりでいますが、そうなっていません。
$(CODE B) のプログラマは、動作を正しく直すために別の関数を $(CODE B) に追加する必要があります:
)
----
class B : A
{
override void foo(long);
override void foo(int);
}
----
$(P しかし、この変更が必要だとは誰も教えてくれません。
$(CODE A) のコンパイル時には、
$(CODE B) が何をオーバーライドするかなどの情報はまったくありませんから。
)
$(P さて、$(CODE A) でどのように仮想関数が呼ばれるかを考えてみましょう。
仮想関数呼び出しは vtbl[] 経由で行われます。$(CODE A) の vtbl[] はこうなっています:
)
----
A.vtbl[0] = &A.foo(long);
A.vtbl[1] = &A.foo(int);
----
$(P $(CODE B) の vtbl[] はこうです:
)
----
B.vtbl[0] = &B.foo(long);
B.vtbl[1] = &A.foo(int);
----
$(P $(CODE A.def()) 内での $(CODE foo(int)) の呼び出しは、
実際には vtbl[1] の呼び出しです。
本当は、$(CODE B) のオブジェクトから $(CODE A.foo(int)) へのアクセスは不可能としたいのです。
解決策としては $(CODE B) の vtbl[] を:
)
----
B.vtbl[0] = &B.foo(long);
B.vtbl[1] = &error;
----
$(P こう書き換えます。実行時にはエラー関数が呼び出され、例外を投げます。
これはコンパイル時にエラーが検出されないという意味で、完璧ではありません。
しかし少なくとも、アプリケーションプログラムが間違った関数を呼んで
そのままそしらぬ顔で実行を続けてしまうということはなくなります。
)
$(P $(I 追記: 現在では、
vtbl[] にエラーエントリが入る場合は、コンパイル時に警告が出るようになっています。)
)
)
$(SECTION2 まとめ,
$(P 関数ハイジャックは、アプリケーションプログラマからはこれを防止する手段がないため、
複雑なC++やJavaのプログラムでは特にやっかいな問題となっています。
言語のセマンティクスにちょっとした修正を加えることで、
表現力やパフォーマンスを失うことなく、これを防止することが可能になりました。
)
)
$(SECTION2 参考文献,
$(UL
$(LI $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/Hijacking_56458.html, digitalmars.D - Hijacking))
$(LI $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/Re_Hijacking_56505.html, digitalmars.D - Re: Hijacking))
$(LI $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/aliasing_base_methods_49572.html#N49577, digitalmars.D - aliasing base methods))
$(LI Eiffel, Scala, C# には override やそれに似た機能があります)
)
$(P Credits:)
$(UL
$(LI Kris Bell)
$(LI Frank Benoit)
$(LI Andrei Alexandrescu)
)
)
)
Macros:
TITLE=関数ハイジャック
WIKI=Hijack
CATEGORY_ARTICLES=$0