- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
doc/generic_types/generic_types.tex
re2ef6bf r2b8a897 278 278 279 279 Finally, \CFA allows variable overloading: 280 %\lstDeleteShortInline@%281 %\par\smallskip282 %\begin{tabular}{@{}l@{\hspace{1.5\parindent}}||@{\hspace{1.5\parindent}}l@{}}280 \lstDeleteShortInline@% 281 \par\smallskip 282 \begin{tabular}{@{}l@{\hspace{1.5\parindent}}||@{\hspace{1.5\parindent}}l@{}} 283 283 \begin{lstlisting} 284 284 short int MAX = ...; 285 285 int MAX = ...; 286 286 double MAX = ...; 287 short int s = MAX; $\C{// select correct MAX}$ 287 \end{lstlisting} 288 & 289 \begin{lstlisting} 290 short int s = MAX; // select correct MAX 288 291 int i = MAX; 289 292 double d = MAX; 290 293 \end{lstlisting} 291 %\end{lstlisting} 292 %& 293 %\begin{lstlisting} 294 %\end{tabular} 295 %\smallskip\par\noindent 296 %\lstMakeShortInline@% 294 \end{tabular} 295 \smallskip\par\noindent 296 \lstMakeShortInline@% 297 297 Here, the single name @MAX@ replaces all the C type-specific names: @SHRT_MAX@, @INT_MAX@, @DBL_MAX@. 298 298 As well, restricted constant overloading is allowed for the values @0@ and @1@, which have special status in C, \eg the value @0@ is both an integer and a pointer literal, so its meaning depends on context. … … 585 585 Tuple flattening recursively expands a tuple into the list of its basic components. 586 586 Tuple structuring packages a list of expressions into a value of tuple type, \eg: 587 %\lstDeleteShortInline@%588 %\par\smallskip589 %\begin{tabular}{@{}l@{\hspace{1.5\parindent}}||@{\hspace{1.5\parindent}}l@{}}587 \lstDeleteShortInline@% 588 \par\smallskip 589 \begin{tabular}{@{}l@{\hspace{1.5\parindent}}||@{\hspace{1.5\parindent}}l@{}} 590 590 \begin{lstlisting} 591 591 int f( int, int ); … … 593 593 int h( int, [int, int] ); 594 594 [int, int] x; 595 \end{lstlisting} 596 & 597 \begin{lstlisting} 595 598 int y; 596 f( x ); $\C {// flatten}$599 f( x ); $\C[1in]{// flatten}$ 597 600 g( y, 10 ); $\C{// structure}$ 598 h( x, y ); $\C{// flatten and structure}$ 599 \end{lstlisting} 600 %\end{lstlisting} 601 %& 602 %\begin{lstlisting} 603 %\end{tabular} 604 %\smallskip\par\noindent 605 %\lstMakeShortInline@% 601 h( x, y ); $\C{// flatten and structure}\CRT{}$ 602 \end{lstlisting} 603 \end{tabular} 604 \smallskip\par\noindent 605 \lstMakeShortInline@% 606 606 In the call to @f@, @x@ is implicitly flattened so the components of @x@ are passed as the two arguments. 607 607 In the call to @g@, the values @y@ and @10@ are structured into a single argument of type @[int, int]@ to match the parameter type of @g@. … … 614 614 An assignment where the left side is a tuple type is called \emph{tuple assignment}. 615 615 There are two kinds of tuple assignment depending on whether the right side of the assignment operator has a tuple type or a non-tuple type, called \emph{multiple} and \emph{mass assignment}, respectively. 616 %\lstDeleteShortInline@%617 %\par\smallskip618 %\begin{tabular}{@{}l@{\hspace{1.5\parindent}}||@{\hspace{1.5\parindent}}l@{}}616 \lstDeleteShortInline@% 617 \par\smallskip 618 \begin{tabular}{@{}l@{\hspace{1.5\parindent}}||@{\hspace{1.5\parindent}}l@{}} 619 619 \begin{lstlisting} 620 620 int x = 10; 621 621 double y = 3.5; 622 622 [int, double] z; 623 z = [x, y]; $\C{// multiple assignment}$ 623 624 \end{lstlisting} 625 & 626 \begin{lstlisting} 627 z = [x, y]; $\C[1in]{// multiple assignment}$ 624 628 [x, y] = z; $\C{// multiple assignment}$ 625 629 z = 10; $\C{// mass assignment}$ 626 [y, x] = 3.14; $\C{// mass assignment}$ 627 \end{lstlisting} 628 %\end{lstlisting} 629 %& 630 %\begin{lstlisting} 631 %\end{tabular} 632 %\smallskip\par\noindent 633 %\lstMakeShortInline@% 630 [y, x] = 3.14; $\C{// mass assignment}\CRT{}$ 631 \end{lstlisting} 632 \end{tabular} 633 \smallskip\par\noindent 634 \lstMakeShortInline@% 634 635 Both kinds of tuple assignment have parallel semantics, so that each value on the left and right side is evaluated before any assignments occur. 635 636 As a result, it is possible to swap the values in two variables without explicitly creating any temporary variables or calling a function, \eg, @[x, y] = [y, x]@. … … 655 656 Here, the mass assignment sets all members of @s@ to zero. 656 657 Since tuple-index expressions are a form of member-access expression, it is possible to use tuple-index expressions in conjunction with member tuple expressions to manually restructure a tuple (\eg rearrange, drop, and duplicate components). 657 %\lstDeleteShortInline@%658 %\par\smallskip659 %\begin{tabular}{@{}l@{\hspace{1.5\parindent}}||@{\hspace{1.5\parindent}}l@{}}658 \lstDeleteShortInline@% 659 \par\smallskip 660 \begin{tabular}{@{}l@{\hspace{1.5\parindent}}||@{\hspace{1.5\parindent}}l@{}} 660 661 \begin{lstlisting} 661 662 [int, int, long, double] x; 662 663 void f( double, long ); 663 x.[0, 1] = x.[1, 0]; $\C{// rearrange: [x.0, x.1] = [x.1, x.0]}$ 664 f( x.[0, 3] ); $\C{// drop: f(x.0, x.3)}$ 665 [int, int, int] y = x.[2, 0, 2]; $\C{// duplicate: [y.0, y.1, y.2] = [x.2, x.0.x.2]}$ 666 \end{lstlisting} 667 %\end{lstlisting} 668 %& 669 %\begin{lstlisting} 670 %\end{tabular} 671 %\smallskip\par\noindent 672 %\lstMakeShortInline@% 664 665 \end{lstlisting} 666 & 667 \begin{lstlisting} 668 x.[0, 1] = x.[1, 0]; $\C[1in]{// rearrange: [x.0, x.1] = [x.1, x.0]}$ 669 f( x.[0, 3] ); $\C{// drop: f(x.0, x.3)}\CRT{}$ 670 [int, int, int] y = x.[2, 0, 2]; // duplicate: [y.0, y.1, y.2] = [x.2, x.0.x.2] 671 \end{lstlisting} 672 \end{tabular} 673 \smallskip\par\noindent 674 \lstMakeShortInline@% 673 675 It is also possible for a member access to contain other member accesses, \eg: 674 676 \begin{lstlisting} … … 859 861 is transformed into: 860 862 \begin{lstlisting} 863 // generated before the first 2-tuple 861 864 forall(dtype T0, dtype T1 | sized(T0) | sized(T1)) struct _tuple2 { 862 T0 field_0; $\C{// generated before the first 2-tuple}$865 T0 field_0; 863 866 T1 field_1; 864 867 }; 865 868 _tuple2(int, int) f() { 866 869 _tuple2(double, double) x; 870 // generated before the first 3-tuple 867 871 forall(dtype T0, dtype T1, dtype T2 | sized(T0) | sized(T1) | sized(T2)) struct _tuple3 { 868 T0 field_0; $\C{// generated before the first 3-tuple}$872 T0 field_0; 869 873 T1 field_1; 870 874 T2 field_2; … … 873 877 } 874 878 \end{lstlisting} 875 Tuple expressions are then simply converted directly into compound literals, \eg @[5, 'x', 1.24]@ becomes @(_tuple3(int, char, double)){ 5, 'x', 1.24 }@. 879 Tuple expressions are then simply converted directly into compound literals: 880 \begin{lstlisting} 881 [5, 'x', 1.24]; 882 \end{lstlisting} 883 becomes: 884 \begin{lstlisting} 885 (_tuple3(int, char, double)){ 5, 'x', 1.24 }; 886 \end{lstlisting} 876 887 877 888 \begin{comment} … … 938 949 In fact, \CFA's features for generic programming can enable faster runtime execution than idiomatic @void *@-based C code. 939 950 This claim is demonstrated through a set of generic-code-based micro-benchmarks in C, \CFA, and \CC (see stack implementations in Appendix~\ref{sec:BenchmarkStackImplementation}). 940 Since all these languages share a subset comprising standard C, maximal-performance benchmarks would show little runtime variance, other than in length and clarity of source code.941 A more illustrative benchmark is the idiomatic costs of each language's features covering common usage.951 Since all these languages share a subset essentially comprising standard C, maximal-performance benchmarks would show little runtime variance, other than in length and clarity of source code. 952 A more illustrative benchmark measures the costs of idiomatic usage of each language's features. 942 953 Figure~\ref{fig:BenchmarkTest} shows the \CFA benchmark tests for a generic stack based on a singly linked-list, a generic pair-data-structure, and a variadic @print@ routine similar to that in Section~\ref{sec:variadic-tuples}. 943 954 The benchmark test is similar for C and \CC. 944 The experiment uses element types @int@ and @pair(_Bool, char)@, and pushes $N=40M$ elements on a generic stack, copies the stack, clears one of the stacks, finds the maximum value in the other stack, and prints $N/2$ (to reduce graph height) constants. 955 The experiment uses element types @int@ and @pair(_Bool, char)@, and pushes $N=40M$ elements on a generic stack, copies the stack, clears one of the stacks, finds the maximum value in the other stack, and prints $N$ values of each type (2 per @print@ call). 956 957 The structure of each benchmark implemented is: C with @void *@-based polymorphism, \CFA with the presented features, \CC with templates, and \CC using only class inheritance for polymorphism, called \CCV. 958 The \CCV variant illustrates an alternative object-oriented idiom where all objects inherit from a base @object@ class, mimicking a Java-like interface; 959 hence runtime checks are necessary to safely down-cast objects. 960 The most notable difference among the implementations is in memory layout of generic types: \CFA and \CC inline the stack and pair elements into corresponding list and pair nodes, while C and \CCV lack such a capability and instead must store generic objects via pointers to separately-allocated objects. 961 For the print benchmark, idiomatic printing is used: the C and \CFA variants used @stdio.h@, while the \CC and \CCV variants used @iostream@; preliminary tests show this distinction has negligible runtime impact. 962 Note, the C benchmark uses unchecked casts as there is no runtime mechanism to perform such checks, while \CFA and \CC provide type-safety statically. 945 963 946 964 \begin{figure} … … 974 992 \end{figure} 975 993 976 The structure of each benchmark implemented is: C with @void *@-based polymorphism, \CFA with the presented features, \CC with templates, and \CC using only class inheritance for polymorphism, called \CCV.977 The \CCV variant illustrates an alternative object-oriented idiom where all objects inherit from a base @object@ class, mimicking a Java-like interface;978 hence runtime checks are necessary to safely down-cast objects.979 The most notable difference among the implementations is in memory layout of generic types: \CFA and \CC inline the stack and pair elements into corresponding list and pair nodes, while C and \CCV lack such a capability and instead must store generic objects via pointers to separately-allocated objects.980 For the print benchmark, idiomatic printing is used: the C and \CFA variants used @stdio.h@, while the \CC and \CCV variants used @iostream@; preliminary tests show this distinction has little runtime impact.981 Note, the C benchmark uses unchecked casts as there is no runtime mechanism to perform such checks, while \CFA and \CC provide type-safety statically.982 983 994 Figure~\ref{fig:eval} and Table~\ref{tab:eval} show the results of running the benchmark in Figure~\ref{fig:BenchmarkTest} and its C, \CC, and \CCV equivalents. 984 995 The graph plots the median of 5 consecutive runs of each program, with an initial warm-up run omitted. … … 1000 1011 & \CT{C} & \CT{\CFA} & \CT{\CC} & \CT{\CCV} \\ \hline 1001 1012 maximum memory usage (MB) & 10001 & 2502 & 2503 & 11253 \\ 1002 source code size (lines) & 247 & 22 3& 165 & 339 \\1013 source code size (lines) & 247 & 222 & 165 & 339 \\ 1003 1014 redundant type annotations (lines) & 39 & 2 & 2 & 15 \\ 1004 1015 binary size (KB) & 14 & 229 & 18 & 38 \\ … … 1008 1019 The C and \CCV variants are generally the slowest with the largest memory footprint, because of their less-efficient memory layout and the pointer-indirection necessary to implement generic types; 1009 1020 this inefficiency is exacerbated by the second level of generic types in the pair-based benchmarks. 1010 By contrast, the \CFA and \CC variants run in roughly equivalent time for both the integer and pair of @_Bool@ and @char@ because the storage layout is equivalent .1021 By contrast, the \CFA and \CC variants run in roughly equivalent time for both the integer and pair of @_Bool@ and @char@ because the storage layout is equivalent, with the inlined libraries (\ie no separate compilation) and greater maturity of the \CC compiler contributing to its lead. 1011 1022 \CCV is slower than C largely due to the cost of runtime type-checking of down-casts (implemented with @dynamic_cast@); 1012 1023 There are two outliers in the graph for \CFA: all prints and pop of @pair@. 1013 1024 Both of these cases result from the complexity of the C-generated polymorphic code, so that the GCC compiler is unable to optimize some dead code and condense nested calls. 1014 A compiler for \CFA could easily perform these optimizations.1025 A compiler designed for \CFA could easily perform these optimizations. 1015 1026 Finally, the binary size for \CFA is larger because of static linking with the \CFA libraries. 1016 1027 1017 \C C performs best because it uses header-only inlined libraries (\ie no separate compilation).1018 \CFA and \CC have the advantage of a pre-written generic @pair@ and @stack@ type to reduce line count, while C and \CCV require it to written by the programmer, as C does not have a generic collections-library and \CCV does not use the \CC standard template library by construction.1019 For \CCV, the definition of @object@ and wrapper classes for @bool@, @char@, @int@, and @const char *@ are included in the line count, which inflates its line count, as an actual object-oriented language would include these in the standard library; 1028 \CFA is also competitive in terms of source code size, measured as a proxy for programmer effort. The line counts in Table~\ref{tab:eval} include implementations of @pair@ and @stack@ types for all four languages for purposes of direct comparison, though it should be noted that \CFA and \CC have pre-written data structures in their standard libraries that programmers would generally use instead. Use of these standard library types has minimal impact on the performance benchmarks, but shrinks the \CFA and \CC benchmarks to 73 and 54 lines, respectively. 1029 On the other hand, C does not have a generic collections-library in its standard distribution, resulting in frequent reimplementation of such collection types by C programmers. 1030 \CCV does not use the \CC standard template library by construction, and in fact includes the definition of @object@ and wrapper classes for @bool@, @char@, @int@, and @const char *@ in its line count, which inflates this count somewhat, as an actual object-oriented language would include these in the standard library; 1020 1031 with their omission the \CCV line count is similar to C. 1021 1032 We justify the given line count by noting that many object-oriented languages do not allow implementing new interfaces on library types without subclassing or wrapper types, which may be similarly verbose. … … 1165 1176 std::forward_list wrapped in std::stack interface 1166 1177 1167 template<typename T> void print(ostream & out, const T& x) { out << x; }1168 template<> void print<bool>(ostream & out, const bool& x) { out << (x ? "true" : "false"); }1169 template<> void print<char>(ostream & out, const char& x ) { out << "'" << x << "'"; }1170 template<typename R, typename S> ostream & operator<< (ostream& out, const pair<R, S>& x) {1178 template<typename T> void print(ostream& out, const T& x) { out << x; } 1179 template<> void print<bool>(ostream& out, const bool& x) { out << (x ? "true" : "false"); } 1180 template<> void print<char>(ostream& out, const char& x ) { out << "'" << x << "'"; } 1181 template<typename R, typename S> ostream& operator<< (ostream& out, const pair<R, S>& x) { 1171 1182 out << "["; print(out, x.first); out << ", "; print(out, x.second); return out << "]"; } 1172 template<typename T, typename... Args> void print(ostream & out, const T & arg, const Args&... rest) {1183 template<typename T, typename... Args> void print(ostream& out, const T& arg, const Args&... rest) { 1173 1184 out << arg; print(out, rest...); } 1174 1185 \end{lstlisting} … … 1209 1220 forall(otype T) struct stack_node { 1210 1221 T value; 1211 stack_node(T) 1222 stack_node(T)* next; 1212 1223 }; 1213 forall(otype T) void ?{}(stack(T) 1214 forall(otype T) void ?{}(stack(T) 1215 stack_node(T) 1216 for ( stack_node(T) 1217 *crnt = ((stack_node(T) 1218 stack_node(T) 1224 forall(otype T) void ?{}(stack(T)* s) { (&s->head){ 0 }; } 1225 forall(otype T) void ?{}(stack(T)* s, stack(T) t) { 1226 stack_node(T)** crnt = &s->head; 1227 for ( stack_node(T)* next = t.head; next; next = next->next ) { 1228 *crnt = ((stack_node(T)*)malloc()){ next->value }; /***/ 1229 stack_node(T)* acrnt = *crnt; 1219 1230 crnt = &acrnt->next; 1220 1231 } 1221 1232 *crnt = 0; 1222 1233 } 1223 forall(otype T) stack(T) ?=?(stack(T) 1234 forall(otype T) stack(T) ?=?(stack(T)* s, stack(T) t) { 1224 1235 if ( s->head == t.head ) return *s; 1225 1236 clear(s); … … 1227 1238 return *s; 1228 1239 } 1229 forall(otype T) void ^?{}(stack(T) 1230 forall(otype T) _Bool empty(const stack(T) 1231 forall(otype T) void push(stack(T) 1232 s->head = ((stack_node(T) 1233 } 1234 forall(otype T) T pop(stack(T) 1235 stack_node(T) 1240 forall(otype T) void ^?{}(stack(T)* s) { clear(s); } 1241 forall(otype T) _Bool empty(const stack(T)* s) { return s->head == 0; } 1242 forall(otype T) void push(stack(T)* s, T value) { 1243 s->head = ((stack_node(T)*)malloc()){ value, s->head }; /***/ 1244 } 1245 forall(otype T) T pop(stack(T)* s) { 1246 stack_node(T)* n = s->head; 1236 1247 s->head = n->next; 1237 1248 T x = n->value; … … 1240 1251 return x; 1241 1252 } 1242 forall(otype T) void clear(stack(T) 1243 for ( stack_node(T) 1244 stack_node(T) 1253 forall(otype T) void clear(stack(T)* s) { 1254 for ( stack_node(T)* next = s->head; next; ) { 1255 stack_node(T)* crnt = next; 1245 1256 next = crnt->next; 1246 1257 delete(crnt); … … 1256 1267 struct node { 1257 1268 T value; 1258 node 1259 node( const T & v, node* n = nullptr ) : value(v), next(n) {}1269 node* next; 1270 node( const T& v, node* n = nullptr ) : value(v), next(n) {} 1260 1271 }; 1261 node 1272 node* head; 1262 1273 void copy(const stack<T>& o) { 1263 node 1264 for ( node 1274 node** crnt = &head; 1275 for ( node* next = o.head;; next; next = next->next ) { 1265 1276 *crnt = new node{ next->value }; /***/ 1266 1277 crnt = &(*crnt)->next; … … 1271 1282 stack() : head(nullptr) {} 1272 1283 stack(const stack<T>& o) { copy(o); } 1273 stack(stack<T> 1284 stack(stack<T>&& o) : head(o.head) { o.head = nullptr; } 1274 1285 ~stack() { clear(); } 1275 stack 1286 stack& operator= (const stack<T>& o) { 1276 1287 if ( this == &o ) return *this; 1277 1288 clear(); … … 1279 1290 return *this; 1280 1291 } 1281 stack & operator= (stack<T>&& o) {1292 stack& operator= (stack<T>&& o) { 1282 1293 if ( this == &o ) return *this; 1283 1294 head = o.head; … … 1286 1297 } 1287 1298 bool empty() const { return head == nullptr; } 1288 void push(const T 1299 void push(const T& value) { head = new node{ value, head }; /***/ } 1289 1300 T pop() { 1290 node 1301 node* n = head; 1291 1302 head = n->next; 1292 1303 T x = std::move(n->value); … … 1295 1306 } 1296 1307 void clear() { 1297 for ( node 1298 node 1308 for ( node* next = head; next; ) { 1309 node* crnt = next; 1299 1310 next = crnt->next; 1300 1311 delete crnt; … … 1309 1320 \begin{lstlisting}[xleftmargin=2\parindentlnth,aboveskip=0pt,belowskip=0pt] 1310 1321 struct stack_node { 1311 void 1312 struct stack_node 1322 void* value; 1323 struct stack_node* next; 1313 1324 }; 1314 1325 struct stack new_stack() { return (struct stack){ NULL }; /***/ } 1315 void copy_stack(struct stack * s, const struct stack * t, void * (*copy)(const void*)) {1316 struct stack_node 1317 for ( struct stack_node 1326 void copy_stack(struct stack* s, const struct stack* t, void* (*copy)(const void*)) { 1327 struct stack_node** crnt = &s->head; 1328 for ( struct stack_node* next = t->head; next; next = next->next ) { 1318 1329 *crnt = malloc(sizeof(struct stack_node)); /***/ 1319 1330 **crnt = (struct stack_node){ copy(next->value) }; /***/ … … 1322 1333 *crnt = 0; 1323 1334 } 1324 _Bool stack_empty(const struct stack 1325 void push_stack(struct stack * s, void* value) {1326 struct stack_node 1335 _Bool stack_empty(const struct stack* s) { return s->head == NULL; } 1336 void push_stack(struct stack* s, void* value) { 1337 struct stack_node* n = malloc(sizeof(struct stack_node)); /***/ 1327 1338 *n = (struct stack_node){ value, s->head }; /***/ 1328 1339 s->head = n; 1329 1340 } 1330 void * pop_stack(struct stack* s) {1331 struct stack_node 1341 void* pop_stack(struct stack* s) { 1342 struct stack_node* n = s->head; 1332 1343 s->head = n->next; 1333 void 1344 void* x = n->value; 1334 1345 free(n); 1335 1346 return x; 1336 1347 } 1337 void clear_stack(struct stack * s, void (*free_el)(void*)) {1338 for ( struct stack_node 1339 struct stack_node 1348 void clear_stack(struct stack* s, void (*free_el)(void*)) { 1349 for ( struct stack_node* next = s->head; next; ) { 1350 struct stack_node* crnt = next; 1340 1351 next = crnt->next; 1341 1352 free_el(crnt->value); … … 1349 1360 \CCV 1350 1361 \begin{lstlisting}[xleftmargin=2\parindentlnth,aboveskip=0pt,belowskip=0pt] 1351 stack::node::node( const object & v, node* n ) : value( v.new_copy() ), next( n ) {}1352 void stack::copy(const stack 1353 node 1354 for ( node 1362 stack::node::node( const object& v, node* n ) : value( v.new_copy() ), next( n ) {} 1363 void stack::copy(const stack& o) { 1364 node** crnt = &head; 1365 for ( node* next = o.head; next; next = next->next ) { 1355 1366 *crnt = new node{ *next->value }; 1356 1367 crnt = &(*crnt)->next; … … 1359 1370 } 1360 1371 stack::stack() : head(nullptr) {} 1361 stack::stack(const stack 1362 stack::stack(stack 1372 stack::stack(const stack& o) { copy(o); } 1373 stack::stack(stack&& o) : head(o.head) { o.head = nullptr; } 1363 1374 stack::~stack() { clear(); } 1364 stack & stack::operator= (const stack& o) {1375 stack& stack::operator= (const stack& o) { 1365 1376 if ( this == &o ) return *this; 1366 1377 clear(); … … 1368 1379 return *this; 1369 1380 } 1370 stack & stack::operator= (stack&& o) {1381 stack& stack::operator= (stack&& o) { 1371 1382 if ( this == &o ) return *this; 1372 1383 head = o.head; … … 1375 1386 } 1376 1387 bool stack::empty() const { return head == nullptr; } 1377 void stack::push(const object 1388 void stack::push(const object& value) { head = new node{ value, head }; /***/ } 1378 1389 ptr<object> stack::pop() { 1379 node 1390 node* n = head; 1380 1391 head = n->next; 1381 1392 ptr<object> x = std::move(n->value); … … 1384 1395 } 1385 1396 void stack::clear() { 1386 for ( node* next = head; next; ) {1387 node 1397 while ( node* next = head; next; ) { 1398 node* crnt = next; 1388 1399 next = crnt->next; 1389 1400 delete crnt;
Note: See TracChangeset
for help on using the changeset viewer.