Question | Mark |
1. Static and dynamic binding [10 marks] | |
2. OO and let over lambda [10 marks] | |
3. Type checking and conversions [10 marks] | |
4. List implementations [10 marks] | |
5. Dynamic allocation [10 marks] | |
6. Macros [10 marks] | |
7. Lexical and dynamic scope [10 marks] | |
8. Higher order functions [10 marks] | |
9. Smart pointers [10 marks] | |
10. Lisp implementation [10 marks] | |
Exam Total (best 9) [90 marks] |
Question 1: Static and dynamic binding [10]
Do the following for lisp, then again for C++:
For each of the following aspects of the language, identify whether binding takes place dynamically or statically. If it takes place dynamically then also identify whether or not it can subsequently be changed later in the same run. Briefly justify each answer.
Sample Solution
Lisp:
(1) dynamic, can't change once set:
memory allocation for a local takes place during execution,
but that assigned space isn't altered/reallocated during the
lifetime of that variable
(2) static:
macros are applied BEFORE compilation takes place,
after which all we have is the revised source code
(3) tricky:
common lisp doesn't actually have local constants, just global,
changes to their values are not permitted directly by the compiler,
e.g. it will disallow the following:
(defconstant x 1)
(setf x 2)
but we can use the pointer-based implementation of lists to work around this:
(defconstant x 1)
(defvar L (list x))
(setf (car L) 2)
; x is now 2!
C++:
(1) dynamic, can't change once set:
memory for a local variable is created on the stack when the enclosing
function is called, and remains allocated (in the same location) until
the function ends
(2) same as lisp
(3) similar to lisp:
local constants are created on the stack along with local variables,
and direct changes are prevented by the compiler (like lisp) but we
can change the contents of local constants through pointers to them
Question 2: OO and let over lambda [10]
In C++ we can overload the constructor for an object, providing a variety of ways to initialize the object.
Provide a lisp example using let-over-lambda to achieve a similar effect, and briefly explain any disadvantages of the let-over-lambda approach compared to the C++ approach.
Sample Solution
; example for a circle constructor,
; accepts optional parameters for
; x,y coordinates of centre point
; r radius of circle
; these values are checked/applied just
; prior to the creation of the dispatcher
; ignores any additional values passed
(defun circle (&optional (x 0) (y 0) (r 1) &rest ignore)
(let ((ctrX 0) (ctrY 0) (radius 1))
(labels
( ; any local methods go here, print given as an example
(print () (format t "(~A,~A):~A~%" ctrX ctrY radius)))
; use optional params to set circle fields
(if (numberp x) (setf ctrX x))
(if (numberp y) (setf ctrY y))
(if (and (numberp r) (> r 0)) (setf radius r))
; now define/return the dispatcher
(lambda (cmd .....))))
; sample uses
(defvar C1 (circle)) ; uses defaults for all three
(defvar C2 (circle 5)) ; uses 5 for x, defaults for others
(defvar C3 (circle 1 2 3 4 5)) ; uses x=1, y=2, r=3, ignores others
(defvar C4 (circle "foo" 10) ; uses defaults for x,r, 10 for y
(funcall C1 'print)
(funcall C2 'print)
etc
Disadvantages:
- the big disadvantage is that no inheritance mechanism is supported
in this fashion (and thus no dynamic binding of methods either)
- there is no implicit mechanism for handling destructors, if any
action was desired beyond simple elimination of the memory space
then the programmer would have to ensure they specifically make
the necessary call at the desired time
- the funcall syntax is a little clumsier than C1.print()
- readability is better/worse than C++ (depending on your point of view)
Question 3: Type checking and conversions [10]
Sample Solution
(1) type checking:
- in C++ type checking takes place at compile time
- in lisp type checking takes place at run time at
the point when we try to use the value passed,
but the programmer also has the ability to perform
their own run-time type checking, e.g. with
functions like (listp L), (stringp S), etc
- this means that run time performance suffers more in lisp,
as the type checking is being done "live" during execution
rather than by the compiler at compile time
- it also means the C++ programmer is warned during compilation
about type mismatches, rather than at runtime, but has less
flexibility/control over how to handle data types than does
the lisp programmer
(2) type casting (programmers explicity requesting a type conversion):
- in C++ this is done with the syntax (typename)(value), e.g.
(float)(x)
- in lisp this is done with the syntax (coerce value typename), e.g.
(coerce x 'float)
type conversions:
- in lisp, since it is dynamically typed, the actual type conversions
generally take place at the point where a value is actually being
used in an operation, rather than at the point of assigning it to a
storage location (until that point the value can simply be stored as
the most suitable type)
- otherwise type conversions (whether implicit or as a result of a cast)
behave similarly in both languages, though lisp can give the appearance
of supporting a wider range of implicit conversions simply because some
operators/functions perform their own type checking/conversions
internally at run time
Question 4: [10] List implementations
The C implementation of common lisp actually uses linked lists/pointers to represent lisp lists, but uses arrays to represent lisp vectors.
Discuss the impact this should have on the decision-making process for lisp programmers who are trying to choose between using lists or vectors in their solution for a particular problem.
Sample Solution
(1) depending on what we plan to do with the data we are storing,
which form we pick could have a noticable impact on runtime:
- if a vector is being stored internally as a C-style array then
accessing elements by position is an O(1) operation
on the other hand, if lists are implemented linked-list style then
accessing elements by position in an O(N) operation
- if we are inserting or removing elements from the front of the
vector then we need to shuffle all the remaining elements forward
or backward, O(N), whereas it is a constant time operation with
the list
- some other operations involve a tradeoff, e.g. inserting an element
in the midst of a vector involves shuffling the remaining elements, O(N),
whereas in a list it can take O(N) time to get to the position but is
then a constant time operation to actually do the insert
- since resizing a vector likely involves a create/copy/destroy approach,
we should think carefully about the choice to use vectors if we expect
frequent resizing
(2) the choice of format also has an impact on storage requirements
- the linked list format needs to store the pointers as well as the data,
whereas the vector (array) format does not
- the array format requires a contiguous section of memory large enough
to hold the entire vector, whereas the linked list format does not
Question 5: Dynamic allocation [10]
In lectures we discussed the gnu malloc approach to handling dynamic memory allocation and deallocation in C.
In your own words, summarize the key issues and the approach taken to addressing them.
Sample Solution
***NOTE*** this sample solution is more detailed than I would expect from a student solution
Issues:
- quickly selecting the "right" chunk to allocate in response
to a request
- quickly handling the release of a previously-allocated chunk
- preventing the fragmentation of memory into a large number
of tiny chunks
- supporting quick threaded requests/releases
Approaches:
(1) structuring both the free and allocated chunks in a manner that
allows traversals and allows easy consolidation of adjacent free chunks
(2) establishing a search structure to quickly find the right chunk for a request
(3) dividing memory into arenas, each governing subsequent requests/releases
that take place within their region, so different threads can get simultaneous
requests serviced by dealing with different arenas
(4) establishing an algorithm to apply to (1) and (2) when requests or
releases take place
(1) Free chunks are stored with their size at both ends, and pointers to the
next and previous free chunks in their respective lists, e.g.
+------+----------+---------+--------------+------+
| size | back ptr | fwd ptr | ...unused... | size |
+------+----------+---------+--------------+------+
Allocated chunks also store their size at each end, along with flags
to indicate how theiy were allocated, e.g.
+------+-----+-----------------------------+------+
| size |flags| ... usable data space ... | size |
+------+-----+-----------------------------+------+
The two formats allow traversals forward and backward through either the
set of all memory chunks or the set of free memory chunks, and allow the
identification of consecutive free chunks so they can be consolidated.
(2) An extra data structure is used to keep track of the start of:
- a cache of free memory for each thread
- the "fastbin" lists of small chunks: e.g. one list for the 32-byte free
chunks, one for the 64-byte free chunks, etc
- a search tree structure for lists of larger specific sizes
- a list of unsorted chunks (for newly released items)
- really big requests are handled through mmap requests direct to the os
(3) Initially all of (heap) memory might be treated as a single arena,
but as the number of overlapping requests from different threads increases
it can be subdivided into ranges, with independent processes assigned control
of each range (arena) for all subsequent requests/releases within that range
(4) allocation algorithm:
see if it can be exactly satisfied from the thread cache
if it's a big request use mmap from the os
otheriwse if it is small enough check the fastbin for the right fit
otherwise move all the fastbin stuff to unsorted,
go through the unsorted chunks and consolidate them,
inserting them into the search tree
find the appropriate chunk in the search tree,
if it is too large then break off the extra
and treat like a release
release algorithm:
store it in the thread cache if there is space
otherwise, if it is small enough, place it in the right fastbin
otherwise if it was mmapped then unmap it
see if it is adjacent to another free chunk, if so consolidate
place the chunk in the unsorted list
Question 6: [10] Macros
Sample Solution
(1) C macros are perhaps simpler to apply and to implement than
lisp macros, since they are simply text/pattern substitution.
The conditional aspects of C macros (ifdef/ifndef etc) provide
a control that is not (or at least not obviously) given through
lisp macros.
(2) lisp macros are more flexible, and easier to apply to a wider
range of situations than C++ templates - including better/more
general support for recursion, identifier generation through gensym,
and giving fully programmable rewrites of the source code, rather
than just basic type substitution.
(3) C++ templates are more powerful and flexible than C #define macros,
in that they are easily applied to more generalized patterns (classes,
functions, methods) than C macros -- essentially saying "in the next
templated item, substitute the programmer-provided type T for pattern Y
throughout. They also provide simpler/clearer recursive potential.
Question 7: Lexical and dynamic scope [10]
In your own words, describe the difference between lexical and dynamic scoping, and explain two different approaches to implementing dynamic scoping.
Sample Solution
Lexical Scope:
The variables visible/accessible at any point in a program are
determined by the set of nested scopes enclosing the current point,
we can access variables previously declared in any of those enclosing
scopes.
Example:
Suppose we have a global variable w,
and we have a function f, with a local variable x,
inside f we have a loop with a local variable y,
inside the loop we have a block with a local variable z
-- inside that block we can access any of the four variables, w, x, y, z
Which variables are accessible can be precisely determined at compile time
from the nesting structure of the code.
Dynamic Scope:
In addition to being able to access the variables within the
current lexical scope(s), within a function we are also able to
access the variables declared within the preceeding call chain.
For example, if function f calls function g, which calls function h,
then inside function h we can also access any variables declared in
functions g or f.
Which specific variables are visible at any point in the code may
depend on the sequence of function calls that brought us to that point,
and cannot necessarily be predicted at compile time.
Some implementation approaches:
(1) For each variable name in use within the program,
maintain a stack of the values associated with that name.
When a variable is created, e.g. when you enter the block in
which it is declared, push the variable's value on the stack.
When the variable is destroyed (e.g. when you finally leave its
block) pop the variable off the stack.
The "current" version of the variable will always be the one
on the top of the stack.
(2) Maintain a list of variable names and their values (possibly also
their types), sorted by order of nesting, and when you call or return
from a function, or when you enter/leave a block, pass the (updated)
list as an implicit parameter.
(3) Maintain a global table of scopes, the variables declared within them,
and their current values (again, possibly also their types).
Maintain a list of the current nesting of scopes, updating it whenever
you call/return from a function or enter/leave a block.
Whenever a variable is referred to, go through your list of scopes to
identify the most local one that contains the desired variable name.
Question 8: Higher order functions [10]
Give an example of a higher order function in lisp, and outline a way to get similar behaviour using function pointers and templated functions in C++.
Explain any drawbacks with your C++ solution.
Sample Solution
Lisp's sort function takes a comparison function as a parameter:
(sort '(4 2 1 5 10) '<)
We came up with something very similar, ssort, in lab 10:
template <class T>
void ssort(T arr[], int size, bool (*compare)(T x, T y))
{
...
if ((*compare)(arr[j], min) ... // using the passed function
...
}
Disadvantages:
- the function pointer syntax needed to create our higher order
function is more complex/error prone than lisp's built-in support
- due to C++'s need for compile-time type checking, the function
passed as a parameter must exactly match the profile given by the
function pointer, whereas in lisp we can pass any function at all
- we need to either instantiate versions of the templated function to
match every compare profile supported, or we must place the template
itself in the .h file to be included by every program that plans on using
the template (no such instantiation is required in lisp)
Question 9: Smart pointers [10]
For any two of the four pointer problems listed below, give a short C/C++ code snippet that illustrates the problem and a short snippet of code using smart pointers to solve/avoid the problem:
Sample Solution
for each of the four problem examples below
I'm assuming there exists a class named Object
with a method named print
dangling pointer:
Object *p = new Object;
delete p;
p->print();
null pointer:
Object *p = null;
p->print();
memory leak:
Object *p = new Object[5];
p = new Object[10];
wild pointer:
Object *p;
p->print();
smart pointers:
// assuming we have the following declared:
std::tr1::shared_ptr<Object> p;
// wild pointers caught:
p->print();
// null pointers caught:
p.reset();
p->print();
// dangling pointers caught:
p.reset(new Object);
p.reset();
p->print();
// memory leaks caught:
p.reset(new Object);
p.reset(new Object); // inaccessible original object deleted
Question 10: Lisp implementation [10]
On the following page are four snippets of code taken from cmpinclude.h in the C implementation of gnu common lisp (gcl).
Explain which of the four you find the most surprising and why - in terms of what the code snippet reveals about the implementation of key lisp function(s) and/or data types.
Sample Solution
Which of the four you choose as "most surprising" is of course up to you,
but here are some of my thoughts:
(1) I actually found this the least surprising, given that we already
know lisp handles lists as a linked lists, with separate portions for
the head element and tail list. It does demonstrate the idea that
there is a generic underlying "object" type to represent any possible
form of lisp data value.
(2) This is interesting in that it shows the implementation of the generic list
"object" data type as a union of all possible specific data types, and revealing
that there is a unique struct associated with the implementation of each of them.
(3) Given part (2) above I would regard (3) as less surprising - giving a named
enumeration of the different supporting types. It does provide us with a
mapping of the different lisp numeric types to the underlying C types.
(4) This gives us a clearer understanding of how exactly lisp is applying
its different primitive operators (~ | ^ / % etc) as combinations of the
underlying C operators. This also shows why/where operations crash when
passed invalid data types.
// (1) the handling of cons/car/cdr #define MMcons(x,y) make_cons((x),(y)) #define MMcar(x) (x)->c.c_car #define MMcdr(x) (x)->c.c_cdr #define CMPcar(x) (x)->c.c_car #define CMPcdr(x) (x)->c.c_cdr #define CMPcaar(x) (x)->c.c_car->c.c_car #define CMPcadr(x) (x)->c.c_cdr->c.c_car #define CMPcdar(x) (x)->c.c_car->c.c_cdr #define CMPcddr(x) (x)->c.c_cdr->c.c_cdr struct cons { object c_cdr; object c_car; }; object make_cons(object,object); object car(object); object cdr(object); object caar(object); object cadr(object); object cdar(object); object cddr(object); | // (3) the handling of data types enum type { t_cons, t_start = 0, t_fixnum, t_bignum, t_ratio, t_shortfloat, t_longfloat, t_complex, t_stream, t_pathname, t_string, t_bitvector, t_vector, t_array, t_hashtable, t_structure, t_character, t_symbol, t_package, t_random, t_readtable, t_cfun, t_cclosure, t_sfun, t_gfun, t_vfun, t_afun, t_closure, t_cfdata, t_spice, t_contiguous, t_end=t_contiguous, t_relocatable, t_other }; typedef int bool; typedef long long lfixnum; typedef unsigned long long ulfixnum; typedef long fixnum; typedef unsigned long ufixnum; typedef float shortfloat; typedef double longfloat; |