forked from HowardHinnant/papers
-
Notifications
You must be signed in to change notification settings - Fork 5
/
contracts-on-virtuals-for-mvp.html
934 lines (795 loc) · 37.3 KB
/
contracts-on-virtuals-for-mvp.html
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
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
<!DOCTYPE HTML>
<html>
<head>
<title>Contracts on virtual functions for the Contracts MVP</title>
<style>
p {text-align:justify}
li {text-align:justify}
blockquote.note
{
background-color:#E0E0E0;
padding-left: 15px;
padding-right: 15px;
padding-top: 1px;
padding-bottom: 1px;
}
ins {color:#00A000}
del {color:#A00000}
</style>
</head>
<body>
<address align="right">
Document number: P3165R0
<br/>
Audience: SG21
<br/>
<br/>
<a href="mailto:[email protected]">Ville Voutilainen</a><br/>
2024-02-26<br/>
</address>
<hr/>
<h1 align="center">Contracts on virtual functions for the Contracts MVP</h1>
<h2>Abstract</h2>
<p>
This paper proposes adding contracts on virtual functions, to the
contracts MVP. The overall rationale is that we shouldn't postpone
something as important as that post-MVP, as that would violate
various completeness design principles of C++, including supporting
all the major techniques and styles of C++. Virtual functions
remain a facility of fundamental importance in C++, and not supporting
contract checks on virtual functions would be untoward enough
to be called a travesty.
</p>
<p>
In a nutshell, the proposed approach is to not inherit base class
virtuals' contracts into the definition of an override, in any way. If
the function is called bypassing the virtual call mechanism, i.e.
with explicit qualification, only the function's own contracts
are evaluated. If a function is called via the virtual call mechanism,
the contracts of the base function in the type used for the call
are evaluated, in addition to the function's own contracts.
</p>
<h2>1. Design goals</h2>
<p>
This proposal has the following design goals, ones that some could
call design principles:</p>
<ul>
<li>the approach must work with existing ABIs, so the proposal should have no ABI impact</li>
<li>the "canonical" substitutable designs must be relatively easy to express, but do not need to be enforced, because..</li>
<li>..the design must allow less-canonical designs, for instance where an overrider has a stricter contract than its base</li>
<li>the approach must not require elaborate proof of subsumption of contracts in a hierarchy</li>
<li>the approach must seek to maintain as much of the possibility to do caller-side checking as possible, but no more</li>
</ul>
<p>The rationale for these is straightforward:</p>
<ul>
<li>It would be undesirable to the point of being unacceptable if adding,
removing, or changing contracts in a class hierarchy causes programs to
no longer link. Don't get me wrong, it's not hard to imagine where
something like that could be beneficial, but it's also very easy to not
just imagine but actually find real-life scenarios where such breakage
would be completely unacceptable, and prevents ever adding contracts
to any virtually-dispatched API.</li>
<li>There are very reasonable designs where the contract of an override is not exactly the same as that of the base, and very plausible cases of that are
such where the derived class requires additional (pre)conditions to be
established, conditions that cannot necessarily be reasonably expressed
as contracts of the base function, if at all.
</li>
<li>We must, however, support common designs, and designs where overriders
have exactly the same contract as their base are common, even when
such overrides are called directly, bypassing the virtual call mechanism.
</li>
<li>It should hopefully be very easy to understand why there's a design
goal of not requiring subsumption proofs. We are talking about
arbitrarily complex expressions with arbitrary runtime behavior,
so computing subsumption proofs for those would, if doable at all,
require much more than we have in the MVP, and much more than
we can ostensibly expect having, and restrictions that we don't
want to have.</li>
<li>The possiblity to do caller-side checking is attractive, we shouldn't
just drop that possibility if we can avoid doing so. There are, of
course, some limitations to how much of that can be done for
a hierarchy.</li>
</ul>
<h2>2. The proposed semantics</h2>
<p>As hinted in the abstract, the idea is that a virtual call checks
the contracts of the overrider it dispatches to, and the base function
contract of the type used for the call as well,
whereas a direct qualified call checks only the
contracts of the function it's calling.</p>
<p>The contract on an overrider is completely independent from the
contract of the base function. That is, it need not be the same,
it need not have any sort of subsumption relationship with the
contract of the base function, it can be wider, it can be narrower.
And there is no inheritance of the base contract into the contract
of the overrider.</p>
<p>Preconditions and postconditions are handled in a similar fashion;
the preconditions and postconditions of the "entry point" used
for the call are checked, and preconditions of the final overrider
actually called are checked after the preconditions of the base function,
and the postconditions of the final overrider are checked before
the postconditions of the base function.
</p>
<p>The intent is to check just the contract-pair of an "entry point" and final
overrider, and there is no checking of a chain of contracts in the hierarchy
of intermediate classes that are bases of the class of the final overrider
and inherit the class of the "entry point". The reason for this is three-fold:</p>
<ul>
<li>the field isn't green, this mechanism aims to (following pun intended)
slot in to the existing virtual call mechanism, and an important
goal is that there is no ABI impact, no additional vtbl slots needed,
no ping-pong between the "entry point" view and the final overrider
view.</li>
<li>I'm not sure whether there are other strong reasons. From a certain
perspective, a virtual call is conceptually an interface-implementation
pair, and intermediate layers don't play into it. Trying to get
them to play seems to suggest elaborate mechanisms that don't necessarily
manage to meet the "no ABI impact required" goal. A call site doesn't
know how deep a hierarchy it's calling in to, the final overrider site
doesn't know which layer in a hierachy was the "entry point". There
are existing cases where adding or removing layers is neither an API
nor an ABI break, it's unclear how much of that could be retained
with a more elaborate chaining mechanism.
</li>
</ul>
<p>Let's go straight into an illustrating example:</p>
<blockquote><pre> // Example 2.1:
struct Vehicle
{
virtual void drive(int speed) pre(speed_within_limit(speed)); // #2.1.1
};
struct MotorVehicle : Vehicle
{
bool engineRunning = false;
void drive(int speed) pre(engineRunning) override; // #2.1.2
};
void use1(Vehicle* veh)
{
veh->drive(80); // #2.1.3
}
void use2()
{
MotorVehicle mv;
use1(&mv);
mv.drive(400); // #2.1.4
}
</pre></blockquote>
<p>So, the intent is that the use at #2.1.3 checks both the contract at #2.1.1 and the
contract at #2.1.2. In other words, it's a virtual call, it dispatches to
#2.1.2, but since it was called via the virtual call mechanism, it also
checks the base contract.</p>
<p>The use at #2.1.4 is still conceptually a call using the virtual dispatch
mechanism, but it checks only the contract at #2.1.2. It's
not a direct call of that function, but since the type used for the call
is the same as the type the overrider is a member of, or in other words,
the static and the dynamic type are the same, there's effectively
just one contract to check.
</p>
<p>Okay then. Let's add more layers:</p>
<blockquote><pre> // Example 2.2:
struct Vehicle
{
virtual void drive(int speed) pre(speed_within_limit(speed)); // #2.2.1
};
struct WheeledVehicle : Vehicle
{
bool tiresSufficientlyInflated = false;
void drive(int speed) pre(tiresSufficientlyInflated) override; // #2.2.2
};
struct MotorVehicle : WheeledVehicle
{
bool engineRunning = false;
void drive(int speed) pre(engineRunning) override; // #2.2.3
};
void use1(Vehicle* veh)
{
veh->drive(80); // #2.2.4
}
void use2()
{
MotorVehicle mv;
use1(&mv);
mv.drive(400); // #2.2.5
}
</pre></blockquote>
<p>The intent is that the use at #2.2.4 checks the contract at #2.2.1
and the contract at #2.2.3. In other words, it's a virtual call,
it dispatches to #2.2.3, but since it was called via the virtual call mechanism,
it also checks the base contract of the type used for the call.</p>
<p>The use at #2.2.5 is still conceptually a call using the virtual dispatch
mechanism, but it checks only the contract at #2.2.3. It's
not a direct call of that function, but since the type used for the call
is the same as the type the overrider is a member of, or in other words,
the static and the dynamic type are the same, there's effectively
just one contract to check.
</p>
<p>Okay, fine, these are what could be called "sunshine scenarios", nothing
too difficult to understand in any of that, although the way the
direct calls work may already be debatable to some. But hold that thought,
let's rain on this parade for a little bit, with explanations coming later:</p>
<blockquote><pre> // Example 2.3:
struct Vehicle
{
virtual void drive(int speed) pre(speed_within_limit(speed)); // #2.3.1
};
struct WheeledVehicle : Vehicle
{
bool tiresSufficientlyInflated = false;
void drive(int speed) pre(tiresSufficientlyInflated) override; // #2.3.2
};
struct MotorVehicle : WheeledVehicle
{
bool engineRunning = false;
void drive(int speed) pre(engineRunning) override; // #2.3.3
};
void use1(WheeledVehicle* veh)
{
veh->drive(80); // #2.3.4
}
void use2()
{
MotorVehicle mv;
use1(&mv);
mv.drive(400); // #2.3.5
}
</pre></blockquote>
<p>Observe the difference in the parameter of use1(): it now takes a
WheeledVehicle*, not a Vehicle*.
</p>
<p>
Here, the call at #2.3.4 checks a contract at #2.3.2 and #2.3.3, and not the contract
at #2.3.1.
</p>
<h2>3. How should this be implemented?</h2>
<p>
The expected implementation strategy is such that for a particular
member function, the contracts attached to a function itself are handled
as usual, so for definition-side checking, the function definition
is amended with a call to an internal function that checks those
contracts. And that's it. The implementation is no different,
considering definition-side checking, than checking any contract.
</p>
<p>For the part of checking the contract of a base function based
on the class used for the call, client-side checking is a plausible
and likely implementation strategy. It is of course also fully
conforming to just treat that part as always having the 'ignore'
semantic. In addition, there are plausible ways via which
the whole check need not be completely done on the client-side,
such as calling a contract-checking function generated when
compiling the definition of the base function.</p>
<p>
In order to <em>automatically</em>, without any effort from the programmer,
also check the contract on a base function, an implementation might
do it so that it checks those contracts on the call site. But it
doesn't have to, because an implementation can decide that those
contract checks always have the "ignore" semantic, and that's
fine. We'll get to how programmers can make checks more guaranteed
later.
</p>
<h2>4. First look back at our design goals and our proposed semantics</h2>
<p>At this point we are really going to look at just the first goal, "must
work with existing ABIs, no ABI impact".</p>
<p>As explained, the addition, removal, or modification of a contract
in a virtual function hierarchy should not be an ABI break. It would
be dreadful if it were, that would completely ruin what virtual functions
do, especially the part where you can change a definition without
that affecting the interface, without changing or recompiling your callers.
The proposed semantics are in concert with this goal, there's
either a direct function call in the vtbl, or a type-adjusting thunk
(which is an existing thing), or a (possibly type-adjusting) contract-checking
thunk. The size and the layout of the vtbl don't change if the contracts
change. In other words, there are no special additional vtbl slots,
so there's no churn of the amount of such things changing.</p>
<h2>5. Non-canonical designs</h2>
<p>
Here we go slightly out of order from our initial design goal
listing:</p>
<ul>
<li>..the design must allow less-canonical designs, for instance where an overrider has a stricter contract than its base</li>
</ul>
<p>The approach supports this fine. Since the overrider's contracts
are checked, and that check doesn't incorporate any of the base
functions' checks, the overrider can have a narrower contract,
it can have a wider contract, it can do what it pleases.</p>
<p>Some might find such designs counter-intuitive to the point
of being unnecessary. To me, such designs are compelling - it's
very plausible that a derived class is stateful, and is not
necessarily always in a state where a particular member function
can be called. Having the ability to write preconditions
that check that the object is in a suitable state seems like
a wonderfully useful use case for contracts.</p>
<h2>6. Canonical designs</h2>
<p>In a so-called canonical design, we want an overrider to have
exactly the contract its base has. Now, there's two ways to
skin this cat:</p>
<ol>
<li>If you have no need for writing a contract on an overrider, just
don't. A call via a base pointer/reference will check the contract
on it, and if your implementation has the right stuff, that's all
you need.</li>
<li>However, for at least two reasons, if you
<ul>
<li>want to make sure that your overrider's contract is checked
whenever you compile the definition of the override with
enforce/observe semantics, or</li>
<li>want to repeat the contract of a base function on an overrider
because you want that contract to be eminently visible near the
overrider</li>
</ul>
you can do so. Just refactor a raw expression in a contract into
a predicate function, and call that predicate both in the base
function contract and in the overrider contract.</li>
</ol>
<p>In other words, for the second bullet:</p>
<blockquote><pre> // Example 6.1:
struct Vehicle
{
virtual void drive(int speed) pre(speed_within_limit(speed));
};
struct WheeledVehicle : Vehicle
{
void drive(int speed) pre(speed_within_limit(speed) && tires_inflated()) override;
};
struct MotorVehicle : WheeledVehicle
{
void drive(int speed) pre(speed_within_limit(speed) && tires_inflated() && engine_is_running()) override;
};
void use1(Vehicle* veh)
{
veh->drive(80);
}
void use2()
{
MotorVehicle mv;
use1(&mv);
}
</pre></blockquote>
<p>Here, for every way of calling a MotorVehicle::drive, it has the
same contracts as calling it via a Vehicle*/Vehicle& or
a WheeledVehicle*/WheeledVehicle&.
</p>
<p>Yes, that requires manual orchestration. But it's doable.
It can be made simpler by refactoring the contract of
WheeledVehicle::drive() into a function, and reusing
that in MotorVehicle. If we want to make it simpler still,
we can entertain post-C++26 language extensions that
allow saying "give me the same contract as the base function has".
The important bit here is that it's possible to express that,
and expressing it doesn't require compromising on any of the
other design goals this approach has, namely the one mentioned
before this one, the ability to narrow/widen a contract of an overrider.
</p>
<h3>How important would it be to enforce canonical designs?</h3>
<p>
There's been a lot of debate on this, on the reflectors and elsewhere.
I just wish to point out, without all that much elaboration, that
a narrowed/narrower contract on an overrider seems perfectly reasonable.
If a derived type requires additional setup before it's substitutable
for its base, that is explicable and plausible and reasonable. You
can't always establish all possible state in a constructor, so it's
plausible that sometimes there's room for a bug where an object
of a derived type is passed to code that expects a pointer/reference
to a base, and users expect a certain contract on it, and the object
of a derived type doesn't yet meet those expectations.
</p>
<p>
That's fine. Even in all the papers about substitutability, none
of them say that substitutability is a purely static concept, and can't
have dynamic aspects. A type can be non-substitutable right after
construction, and substitutable once certain additional methods
are performed on it. Most importantly, contracts can <em>check</em>
that, once we indeed allow an overrider to have a narrower contract
than its base function. That seems incredibly useful.
</p>
<p>Thus, summa summarum, considering all the design goals enumerated
in this paper, it's more important to allow meeting all those goals
than to enforce a particular one at the cost of others. If a user
wishes to perform such enforcement, analysis tools and coding guidelines
are a plausible way to get it.</p>
<h2>7. Multiple inheritance</h2>
<p>Multiple inheritance will Just Work, without any additional rules.</p>
<p>If we look at an example like</p>
<blockquote><pre> // Example 7.1:
struct B1 {
virtual void f(int x) pre(x >= 0);
};
struct B2 {
virtual void f(int x) pre(x >= 0 && x < 140);
};
struct D : B1, B2 {
void f(int x) pre(x > 42 && x < 100);
};
void use1(B1* b) {
b->f(66);
}
void use2(B2* b) {
b->f(66);
}
void use3()
{
D d;
use1(&d);
use2(&d);
}
</pre></blockquote>
<p>the example is well-formed and contains no contract violations. The call
in use1() will check the contracts of B1::f() and D::f(), the call in
use2() will check the contracts of B2::f() and D::f().
</p>
<p>Virtual bases and pure virtual functions will also work without any additional special
rules:</p>
<blockquote><pre> // Example 7.2:
struct B {
virtual void f(int x) pre(x >= 0);
};
struct B1 : virtual B {
virtual void f(int x) pre(x >= 0) = 0;
};
struct B2 : virtual B {
virtual void f(int x) pre(x >= 0 && x < 140) = 0;
};
struct D : B1, B2 {
void f(int x) pre(x > 42 && x < 100);
};
void use1(B* b) {
b->f(66);
}
void use2(B* b) {
b->f(66);
}
void use3()
{
D d;
use1(&d);
use2(&d);
}
</pre></blockquote>
<h2>8. Recap</h2>
<h3>The downsides</h3>
<p>This approach</p>
<ul>
<li>doesn't provide any language-level mechanisms to check a canonical
design to be canonical.</li>
<li>thus, if such designs are desired to be verified, some sort of other analysis is necessary, has false positives, and may not be applicable at a wide
scale due to those false positives.</li>
<li>doesn't make it very easy to have both the interface and implementation
contracts of a virtual call verified.</li>
</ul>
<h3>Rebuttals of the downsides, and the upsides</h3>
<p>
The rebuttals:</p>
<ul>
<li>we can have potentially have language-level mechanisms to check a canonical
design to be canonical, post-C++26.</li>
<li>writing canonical designs isn't altogether hard: use a predicate function in a base function contract, use the same predicate in the overrider's contract.</li>
<li>it's unlike to be hard to have both the interface and implementation
contracts of a virtual call verified, compile both the library
and its client with checks turned on.</li>
</ul>
<p>
Further upsides:</p>
<ul>
<li>the model is simple; it's not trivially easy to understand if you have
presumptions about canonical designs on your mind, but it's easy
to explain that the contracts of overrides are independent from the
base contracts.</li>
<li>the rules of the model are simple, the checking of contracts on
overrides requires no special rules, multiple inheritance requires
no special rules, diamond inheritance with a virtual base requires
no special rules.</li>
<li>the model is general and flexible, and closes no extension doors.
It supports all the various use cases brought forward, of which
the ability to narrow a contract on an overrider will be very important
for stateful derived classes.</li>
</ul>
<p>Finally, overall, even if the model isn't the most trivially easy
to understand, it checks all the design goal boxes thrown at it,
and all that is much better than having a contracts ability in C++
that doesn't support one of the fundamentally important parts of
C++. My take on this continues to be that we should adopt
this approach into the Contracts MVP before it's forwarded for
design review by EWG and LEWG.</p>
<h2>9. Wording</h2>
<p>The wording proposed is as a delta against P2900.</p>
<p>In [dcl.contract.func], remove the restriction that
virtual functions can't have contracts:</p>
<blockquote><pre>A coroutine ([dcl.fct.def.coroutine]),
<del>a virtual function ([class.virtual]), </del>a deleted function ([dcl.fct.def.delete]),
or a function defaulted on its first declaration ([dcl.fct.def.default])
may not have a function-contract-specifier-seq.
</pre></blockquote>
<p>
Modify the modification to [expr.call], paragraph 6
<blockquote><pre>When a function is called, each parameter ([dcl.fct])
is initialized ([dcl.init], [class.copy.ctor]) with its corresponding
argument and each precondition assertion([dcl.contract.func)] is evaluated.
<ins>If the selected function is virtual, the precondition assertions
of both the statically chosen function and the final overrider are evaluated.</ins>
</pre></blockquote>
<p>
Modify the modification to [expr.call], paragraph 7</p>
<blockquote><pre>The postfix-expression is sequenced before each expression in the expression-list
and any default argument. The initialization of a parameter,
including every associated value
computation and side effect, is indeterminately sequenced
with respect to that of any other parameter.
These evaluations are sequenced before the evaluation of the precondition
assertions of the <ins>statically chosen</ins> function,
<del>which are evaluated in sequence</del>
<ins>which are, in turn, sequenced before the evaluation of the
precondition assertions of the final overrider, if any.
All precondition assertions of a function are evaluated in sequence ([dcl.contract.func]).</ins>
</pre></blockquote>
<p>Add a new modification to [expr.call], paragraph 8</p>
<blockquote><pre>The result of a function call is the result of the possibly-converted operand
of the return statement (8.7.4) that transferred control
out of the called function (if any), except in a virtual function call
if the return type of the final overrider is different from the return type
of the statically chosen function, the value returned
from the final overrider is converted to the return type of the
statically chosen function.
<ins>Then, in a virtual function call, the postconditions of the
statically chosen function are evaluated in sequence ([dcl.contract.func]).</ins>
</pre></blockquote>
<h2>10. Some Q&A</h2>
<p>Q1:</p>
<p>
The proposed wording mentions postconditions, but the design discussion does not. There should be design discussion about why the proposed wording says what it does about postconditions, and why covariance is not considered important.</p>
<p>A1:</p>
<p>There's been an attempt to explain this in the current revision of this
paper. But in addition to that, there are plausibly useful designs
where the suggested covariance would seem very limiting. We have
plausible use cases for both narrowing and widening preconditions,
presumably we have such use cases for narrowing and widening postconditions
too.</p>
<p>Consider a modified ostream/ofstream pair, a design where output to the
stream isn't just silently ignored if the stream can't put it somewhere
real, but you don't want a hierarchy-cross-cutting virtual call for verifying
it, because you don't need it. You have contracts, you don't need
defined-behavior extra APIs to verify things that contracts can check
without any overhead in 'ignore' 'mode'. Such an ofstream converts
to an ostream, but it has an additional precondition on its output
operations that 'is_open()' is true. That's a final overrider precondition
that is narrower than that of its base function's precondition.
It also has a narrower postcondition, because it's going to actually
check that the output request was conveyed into the buffer, and into
an actual file buffer, not just a generic streambuf.</p>
<p>If we require/mandate/enfore covariance everywhere, you can't express
that kind of designs. But what's even worse, even if you could design
differently, you can't just simply check a postcondition that might
not be covariant, you would need to revamp your class design.</p>
<p>So yes, this provision is indeed something that supports "non-canonical"
designs. For the case of actually <em>widening</em> a postcondition,
an overrider can act as a new base function, establishing a different
contract for itself and its overriders, including a contract where
postconditions simply establish less, they establish fewer
things. But while such designs were sometimes necessary for practical
reasons, they were indeed "type-unsafe" - but now, with contracts, they
no longer are, or they are less unsafe. Because with the proposed
approach, they can be checked and bug-mitigated!</p>
<p>Q2:</p>
<p>The paper does not show examples with base class state that is visible/used in derived classes (directly or via functions). So to ask for one such example: In the first "Vehicle" example, how would the example change if the Vehicle base class stored its current speed as a data member... could the base class author still use contracts to maintain a meaningful invariant for all Vehicle objects?
</p>
<p>A2:</p>
<p>The paper is kinda hinting at it (see Example 6.1), but let's be clearer and more explicit about it:
<blockquote><pre> // Example 10.1:
class Vehicle
{
private:
int current_speed;
public:
bool speed_within_limit(int speed) const;
virtual void drive(int speed) pre(speed_within_limit(speed)); // 10.1.1
};
struct MotorVehicle : Vehicle
{
bool engineRunning = false;
void drive(int speed) pre(engineRunning && Vehicle::speed_within_limit(speed)) override; // 10.1.2
};
void use1(Vehicle* veh)
{
veh->drive(80); // 10.1.3
}
void use2()
{
MotorVehicle mv;
use1(&mv);
mv.drive(400);
}
</pre></blockquote>
<p>The proposal doesn't provide a particular facility for the base
class to enforce that derived classes use the same predicate as the base
does. But there are techniques like the one above that make it relatively
easy to do. Actual enforcing mechanisms are left for future extensions,
although there's no particular guarantee that we'll get such extensions.
But nevertheless, the call at 10.1.3 evaluates both the precondition
at 10.1.1 and the precondition at 10.1.2, because the one at 10.1.1
is evaluated because we called through a Vehicle, so that's the
"entry point"/interface/handle contract check that gets evaluated,
and the final overrider is 10.1.2, so the contract check of that
gets evaluated as well.
</p>
<p>Q3:</p>
<p>More generally: The paper does not mention invariants, and even though the MVP doesn't have invariants, future extensibility is a reasonable question and invariants are important even today... Would the paper also argue that derived class invariants should by default be independent of base class invariants?
</p>
<p>A3:</p>
<p>
Maybe. That depends on how those invariants would work. It's also worth
remembering that base class invariants would need to be accessible
to derived classes for them to be able to check the base invariants.
This proposal doesn't propose any special access rules via which
preconditions and postconditions of bases could just always be checked,
and maybe we shouldn't do that for invariants either. There's
also the case of private bases, where there's ostensibly no need
for a derived class to redo its invariant checks.
</p>
<p>Q4:</p>
<p>
This feels like the rules for template specialization / duck typing (you get the semantics/contracts of the function you happen to match + specializations are not substitutable and need not bear any relationship to the primary template at all), which have generally been viewed as a language weakness that we've been trying to correct (e.g., with concepts, and hang-wringing about vector<bool>). What reasons are there to pursue a design for inheritance that's more like that, than like existing language rules for inheritance (e.g., covariance)?
</p>
<p>Q5:</p>
<p>The paper acknowledges that substitutability is desirable, but argues that contracts should be non-substitutable by default and that manual orchestration should be required to get substitutability. In an era where we are trying to increase C++'s type safety, and reduce type safety bugs rather than create new ways to write them, how would we answer a question like "when C++ finally added contracts why did it make the new feature type-unsafe again"?</p>
<p>The paper argues that substitutability is desirable, <em>in some cases</em>.
There are field experience reports of existing code where strict rules
of that aren't always followed. We could do a substitutability-enforcing
design first, and a relaxation later, but that has two problems:</p>
<ul>
<li>Substitutability doesn't mean that an object is always substitutable
after its construction. Nothing in e.g. Liskov's paper says that. It's
perfectly plausible that there are types where substitutability changes
over time. Overriding functions that have narrower preconditions than
their base are wonderful; they can check perfectly reasonable designs
where an object needs post-construction operations to become
<em>actually</em> substitutable for its base type. There's
<em>nothing</em> wrong with such designs, it's perfectly reasonable
to create a bag of objects in a container without necessarily incurring
the overhead of getting them into such a substitutable state. You don't
start the engines of all of your motor vechicles the moment the vehicles
roll out from the assembly line and into a car dealer's storage.</li>
<li>This approach, whether we all agree on it or not, allows contractifying
the "non-canonical" designs <em>now</em> instead of in an unknown
point in the future. You can start slapping contracts on base class
functions - there's no need to say "don't do that unless your
derived classes have a canonical design". Just go ahead and start
using contracts, the design comes with escape hatches. It comes
with escape hatches by default. :) For some code that we have
field reports of, it makes it much easier and faster to migrate
to contract use.</li>
</ul>
<p>Q6:</p>
<p>Could the paper elaborate more on why the wording says to only fire the pre/post conditions of the static and most-derived types? For example, for a linear hierarchy from most-base A to most-derived E with a virtual function A::f, is it intended that a call with static type B::f that dynamically calls E::f will fire the pre/post conditions for B::f and E::f, but not for A::f, C::f, or D::f?
</p>
<p>A6:</p>
<p>An attempt to elaborate on that point has been made in 2. Proposed semantics.
</p>
<p>Q7:</p>
<p>The paper has no references/citations: What experience with prior art exists for the proposed semantics?
</p>
<p>A7:</p>
<p>
The most important design rationale for these semantics wasn't trying
to be like another language, and no library approach can truly achieve
what preconditions and postconditions do. I'm not sufficiently
familiar with e.g. BSL's assertion mechanisms to be able to say
to what extent and how closely they are able to mimic the language facility.
The design goals were a significant driving factor, the goal of having
no ABI impact was deemed essential. Various other parts of the proposal
were designed with WG21 members' field experience reports in mind, especially
ones about both narrowing and widening preconditions.</p>
<p>Curiously enough, <a href="http://www.ada-auth.org/standards/12rat/html/Rat12-2-3.html">this document about ADA</a> suggests that ADA has both
preconditions and postconditions that are inherited (the "class wide" ones)
and preconditions and postconditions that are <em>not</em> inherited.
In that description, the "class wide" contracts are inherited, and their
full chain "must be true", but it also says thus:</p>
<blockquote>
However, the rules regarding preconditions are perhaps surprising.
The specific precondition Pre for Equilateral_Triangle must be true (checked in the body) but so long as just one of the class wide preconditions Pre'Class for Object and Triangle is true then all is well. Note that class wide preconditions are checked at the point of call. Do not get confused over the use of the word apply. They all apply but only the ones seen at the point of call are actually checked.</blockquote>
<p>
The non-"class wide" contracts are not inherited, and
don't run as a chain.
</p>
<p>
There's also nothing in that writeup that suggests any substitutability
is enforced. There's a "must be true" statement about a "class-wide"
chain, but nothing in any of it says that e.g. a precondition on
an override couldn't be narrower than the one of its base.
</p>
<p>
There's certainly a difference that the contracts described in that
writeup are always attached to a "subprogram"/procedure body,
so abstract procedures can't have contracts. That's certainly
another difference between ours and theirs, there's no restriction
that a pure virtual function can't have contracts - and of course
there is a more fundamental difference that in C++, a pure virtual
function can have a definition.
</p>
<h2>11. A look at some suggested requirements</h2>
<p>These were contributed by another WG21 expert member. Some of them
are certainly contradictory.</p>
<ol>
<li>Allow widening preconditions in derived classes
<ul>
<li>This proposal allows that.</li>
</ul>
</li>
<li>Allow narrowing preconditions in derived classes
<ul>
<li>This proposal allows that.</li>
</ul>
</li>
<li>Ensure substitutability (preconditions cannot be narrowed and postconditions cannot be widened when calling through reference to base)
<ul>
<li>This proposal doesn't include mechanism for such ensuring.</li>
</ul>
</li>
<li> Don't silently inherit contracts (check only the derived function contract when making a non-virtual function call)
<ul>
<li>This proposal does that.</li>
</ul>
</li>
<li> Allow checking both the interface and the implementation contracts when making a virtual function call
<ul>
<li>This proposal does that.</li>
</ul>
</li>
<li>Allow checking contracts through the entire hierarchy of classes when making a virtual function call
<ul>
<li>This proposal does do a full-chain check.</li>
</ul>
</li>
<li>Do not check contracts that are not relevant for the correctness of the given virtual function call
<ul>
<li>This proposal doesn't check non-relevant contracts, for designs
where some contracts in an inheritance hierarchy aren't deemed
relevant.</li>
</ul>
</li>
<li>Avoid an ABI break when adding pre/post to a virtual function
<ul>
<li>This proposal does avoid such ABI breaks, and that's deemed
fundamentally important.</li>
</ul>
</li>
<li>Do not require client-side checking
<ul>
<li>This proposal doesn't require client-side checking, but it's
certainly the most plausible high-QoI implementation strategy
for checking a base function contract for a virtual call.</li>
</ul>
</li>
<li>Do not require recompilation when a contract changes
<ul>
<li>This proposal doesn't require recompiling a base class when
a contract on a derived class changes. Recompiling a derived
class when a base contract changes isn't required either.</li>
</ul>
</li>
<li> Support multiple inheritance where the base class functions have different contracts
<ul>
<li>This proposal supports multiple inheritance with base class functions
from different base classes having different contracts.</li>
</ul>
</li>
<li>Have implementation experience with the chosen strategy
<ul>
<li>We unfortunately do not have implementation experience for this
strategy yet.</li>
</ul>
</li>
<li>Interfaces and implementations should be independent
<ul>
<li>This proposal allows base interfaces and overrider implementations
to be independent.</li>
</ul>
</li>
<li>Avoid adding more symbols to the binary when adding a contract to a virtual function
<ul>
<li>At various times, implementation strategies that rely on
additional functions, exported from library objects, to be used
for e.g. checking a base class contract on a virtual call.
This proposal doesn't require that.
</li>
</ul>
</li>
</ol>
</body>
</html>