Log in

Previous Entry | Next Entry

As most of you know, I have a deep and abiding love for the C++ Standard Template Library. Perhaps paradoxically, the first language I ever got any good with was Python, and I still adore it deeply. It's not the best language for much of the heavy numerical lifting that I have to do, so I mostly use Python for prototyping, and part of what I love about the STL is that it lets me do the same kinds of cool higher-order things I can do in Python.

For instance, suppose I'm working in Python and I have a list of objects, foo. (These could be numbers, strings, files, network sockets, objects I defined, whatever.) I can filter that list based on some arbitrary criterion that I define on the fly: filter(lambda x: x > 13, foo). Instead of the lambda, I could instead use a reference to some other function -- even a method of an object. Consider the following:
>>> foo = ['123', 'abc', '123abc']
>>> filter(str.isalpha, foo)
Better yet, those function references can be handed around just like ordinary variables. I can write a function which takes another function (let's call this one bar) as one of its arguments, and use bar anywhere it can be legally applied. I don't have to know bar's real name -- in fact, bar might not even have a real name, because it could be a lambda function and thus anonymous.

The technical term for this is functions as first-order data. It's a hallmark of the functional programming paradigm, so if you learn Lisp or Haskell, you'll be doing this all the time. Treating functions as first-order data lets you do all kinds of crazy shit: you can compose them, you can bind arguments to them (and perhaps those bound arguments are the results of other functions!), you can return them as results (seriously!), and most importantly, you can construct them at runtime. It provides a level of power, expressiveness, elegance and readability which just isn't found in languages which don't support functions as first-order data.

Which languages don't support functions as first-order data? That would be most of them.

"But Meredith!" I hear you say. "I can pass a function pointer as an argument in C; doesn't that mean C supports functions as first-order data?" Well, no, it really doesn't. A function pointer is a pointer, not a function. You can construct a function pointer at runtime, but you can't construct the underlying function it points to at runtime, so you're stuck. Function pointers are also fugly as hell, and although readability is not a requirement -- see Unlambda as an example of a purely functional language which is also completely unreadable -- it sure is handy.

But let's get back to the STL. C++ also doesn't truly support functions as first-order data, but it comes closer by providing function objects, which it calls "functors". In C++, any class can have an operator() method, which allows objects of that type to behave as functions: pass a suitably-typed argument to the object, and voila, you'll get back a result. Template polymorphism makes this even more powerful: it's quite easy to define, for instance, a functor which takes two arguments of the same type, T a and T b, and returns their sum, a + b. (This works as long as operator+ is defined for type T; if not, the compiler will yell at you.) The STL provides a wide variety of predefined functors, along with ways to combine them to create more sophisticated ones. Best of all, any ordinary function can be instantly promoted to a functor with ptr_fun, and any class member function can become a functor via mem_fun (or its slightly less useful brother mem_fun_ref, but that's another story).

Okay, but why is this useful? Indeed, the <functional> library is rather masturbatory on its own, but it really comes into its own when paired with the <algorithm> library. This bad boy provides several dozen handy algorithms for operating on sequences -- searching, sorting, replacing, copying, swapping, removing, shuffling, reversing, applying arbitrary functors to members, and so on, plus conditional versions of many of these (e.g. find_if and remove_if). These algorithms are all generic: you can use them on any STL container1, and an STL container can contain anything you want2.

Also -- and I am putting this on its own line because it's important -- they are lightning goddamn fast. They are also somebody else's problem3. Many people who are much smarter than I will ever be have put hundreds of hours of work into making sure these libraries work and perform well. Sure, if you really know what you're doing, you can hand-hack yourself a special case in C or assembler and beat the STL's performance, but that requires (1) really knowing what you're doing, (2) the Copious Free Time to do it yourself, and (3) getting down and dirty with a profiler. (If you're trying to tell me that your C will always beat the C++ equivalent but you haven't profiled the two against each other and your name isn't Dennis Ritchie, get the hell off my lawn.)

Anyway. The <algorithm> library means that conditional operations over containers, like "how many even numbers are in this list?", don't require a for-loop -- you can express them in one line (see the example at the end). The catch, however, is that any function you apply in an STL algorithm must be expressed as a functor. So, if you want to start using C++ to its full power, you're going to need to take an hour or two to get comfortable with functors and how they work.

Now, one of the coolest inventions in the C++ world in the last fifteen years was the concept of smartpointers. (I have waxed rhapsodic on this before.) Smartpointers are objects that help programmers avoid the problem of memory leaks. Dynamic allocation functions (malloc) and operators (C++ new) grab memory off the heap and return a pointer to that storage. If the pointer goes out of scope and you haven't explicitly put that memory back with free or delete, the memory is now unreachable by your program or any other one -- congrats, you've created a memory leak. It'll free itself when the program terminates4</sup> ... but will that happen because the user terminated it normally, or because it ate up all available memory, threw bad_alloc and died?

By contrast, since smartpointers are objects, they have constructors and destructors. When they come into scope, they allocate whatever they point to; when they go out of scope, they destroy their target and free up its storage, even in troublesome cases like exception-triggered stack unwinding. They are immensely useful, and they are rather more complicated than I have described them here; indeed, it took some three months for the Boost team to decide how best to implement them, and they weren't accepted into the C++ Standards Committee's Technical Report 1 until 2005, some seven years later. But now they're here, they've been part of libstdc++ since gcc4.05, and they're ready to rock.

They are also where all the trouble started.

I found myself in a coding situation where I needed to maintain a std::vector of objects all belonging to the same abstract class. Doing exactly this is impossible, for two reasons:
  1. A vector<T> needs to know how big T is, in order to allocate its own storage properly. Otherwise, T is an "incomplete type", and the compiler won't let you instantiate that vector.
  2. Abstract classes are incomplete types.
However, pointers are all the same size -- a pointer is just a variable that holds an address. So even if T is an incomplete type, *T isn't, and you can create a vector<T*> just fine -- create your derived-class objects, upcast them, and you're golden.

However, although vectors manage their own memory, a vector of pointers will only release the pointers it contains -- not their contents. I could write a routine to free up the pointers' contents, but could I be sure it would be called if the vector were unexpectedly destroyed? I couldn't, but a smartpointer could do that for me. Better yet, shared_ptr (one of the smartpointer classes described in TR1) will automatically upcast itself whenever it needs to. Result: I created a vector of smartpointers to my base class, filled it with smartpointers to objects of various derived classes, and life was good.

For my next trick, I needed to be able to search my vector for the first object which returned false for a certain parameterless method defined in the base class. In most cases, this is easy: find_if(v.begin(), v.end(), not1(mem_fun(&X::foo))), where X is the class, foo is the method, and not1 is an STL adaptor function6 which provides a functor returning the opposite of whatever its argument (here, the functor created by mem_fun) returns.

Alas, this doesn't work for smartpointers, because a shared_ptr<T> is not a T; it doesn't have T's methods. This isn't the STL's fault; mem_fun entered the standard before shared_ptr did. The TR1 authors noticed this, and helpfully included mem_fn, a cleverer version of mem_fun which plays nicely with smartpointers.

However, they did not provide an update to not1 (or, more properly, its underlying function object adaptor unary_negate), and thus my compiler choked all over the place.7 What's a girl to do?

Well, as cipherpunk pointed out when I brought this up to him after I'd already solved the problem, the canonical solution is to write up a little adaptor class and point not1 at that. I'll leave that as an exercise for the reader; I could do that, but I didn't want to, because, dammit, standard tools should be able to do standard things. There had to be a way -- but what was it?

The documentation for unary_negate provided a clue: a footnote at the end noted that unary_negate could be constructed from logical_not and unary_compose. Well, unary_compose isn't actually part of the Standard; it's an SGI extension, and gcc provides it in the <ext/functional> header, but that left me in the same boat as not1 did. However, TR1 has expanded facilities for function composition, in the form of bind. The STL has limited support for function composition in the form of binder1st and binder2nd; you can bind a constant value to either the first or second argument of a two-argument function, and that's it. TR1's bind -- which, like mem_fn, had its genesis in Boost's bind -- is infinitely more flexible. You can bind as many arguments as you like, to functions of as many arguments as you like. Cooler still, you don't have to bind constant arguments -- you can bind "placeholder arguments", which substitute runtime arguments positionally. _1 is "the first argument given", _2 is the second, and so on and so forth. And, best of all, you can bind other functions. Or functors, if you so choose.

Armed with this knowledge, I tried the following:
find_if(v.begin(), v.end(), bind(logical_not<bool>(), mem_fn(&X::foo)))
That failed too. If you've read this far, you probably understand enough to figure out why; take a moment and see if you can find the bug.

Okay, here's the deal. find_if takes as its last argument a functor, which it applies to every element of the sequence bounded by the iterators that are its first two arguments. bind, as we said, binds arguments to functions (or functors). logical_not is a functor, but it isn't a function adaptor. It takes a boolean value and returns the negation of that value. So, we have to bind a boolean value to it. mem_fn returns a functor. (Unary, in this case.) A functor isn't a boolean value. In this case, it returns a boolean value, but in order for logical_not to work, I have to bind it to a boolean value.

So, how do we turn a unary functor into a boolean value? bind to the rescue again! Use one of those placeholder values I mentioned, and now we have
find_if(v.begin(), v.end(), bind(logical_not<bool>(), bind(&X::foo, _1)))
It compiles, it runs! Hallelujah!

And yet I am disappointed. In principle, I really shouldn't be. What I've written here is very functional in style -- I am quite literally composing a function with another composed function, and I am doing so with strong typing and polymorphism. (If foo were virtual, the appropriate subclass method would be called.) But it is less readable than it ought to be, and I had to do it because my new tools broke my old tools without providing a replacement for the old tools. I expect better from the latest-and-greatest in my language of choice; at the very least I expect not to have to find out the hard way. (Boost's mem_fn documentation provides a brief warning that it isn't 100% compatible with STL adaptors, but apparently this issue is not general knowledge.)

Hmm. I've been asked, in the past, to write articles for programming magazines about projects I've worked on. Perhaps something less long-winded than this diatribe is in order.

1With a few exceptions. Scott Meyers' Effective STL is a wonderful field guide to the STL, its capabilities, and most importantly, its weird edge cases. I recommend it highly.
2Except references. I haven't run into many situations where I'd want a container of references, though.
3Then again, tonight this got me into trouble. Which is actually why I'm so surprised.
4Unless you were using POSIX shared memory, in which case, sucks to be you. man(1) ipcrm or reboot, sucker.
5No love from Visual C++ until whatever the release after Visual Studio 2008 is, alas. Go download Boost.
6A syntactic-sugar function to create a particular function object adaptor, in this case unary_negate. The STL has several of these, to make code easier to read; others include bind1st and bind2nd (which are handy for function composition, but too nuanced for me to explain in a footnote; go read about them) and in fact our aforementioned ptr_fun and mem_fun.
7Reading the output closely, it actually appears to be a problem with argument deduction:
/Developer/SDKs/MacOSX10.4u.sdk/usr/include/c++/4.0.0/bits/stl_algo.h:259: error: no match for call to '(std::unary_negate<std::tr1::_Mem_fn<bool (pqxx::connection_base::*)()const> >) (std::tr1::shared_ptr<pqxx::lazyconnection>&)'
/Developer/SDKs/MacOSX10.4u.sdk/usr/include/c++/4.0.0/bits/stl_function.h:322: note: candidates are: bool std::unary_negate<_Predicate>::operator()(const typename _Predicate::argument_type&) const [with _Predicate = std::tr1::_Mem_fn<bool (pqxx::connection_base::*)()const>]
Looks to me like unary_negate's operator() doesn't like accepting a shared_ptr to a derived class, or else mem_fn isn't providing an argument_type. OTOH, if this is a const mismatch, I am going to go throw someone out a window.


( 38 comments — Leave a comment )
Nov. 1st, 2007 10:09 am (UTC)
After reading almost every byte of this posting the thing that sticks out most to me is...

Perhaps paradoxically, the first language I ever got any good with was Python

If you can do it all in Python you don't have to worry about C++/templates/STL idiosyncrasies!
Nov. 1st, 2007 10:38 am (UTC)
Heh. You can, but Python isn't really the language of choice for hardcore numerical computation.
Nov. 1st, 2007 11:18 am (UTC)
At least not without losing (some of) the advantages of Python. I've had some success doing (some) numerical stuff in Common Lisp, but that route (alas) means a lower amount of ready-made libraries. Not a major problem for me, but it may be for some.
Nov. 1st, 2007 02:38 pm (UTC)
Lisp doesn't so much have a dearth of ready-made libraries as it does a dearth of well-maintained ready-made libraries. I like knowing that if one of the people maintaining a library got hit by a bus, the library would still be maintained and actually have seen some development six months from now.
Nov. 1st, 2007 03:03 pm (UTC)
There is that. At least the GENHASH library has a (now) stable API and if I ever get hit by a bus, the library hasn't needed an update since, well, April apparently. Still, I think every single CL library I've written has a strict bus number of 1 (most are fairly stable, though, and should be easy enough for anyone to take over).
Nov. 2nd, 2007 02:04 am (UTC)
It's been a while since I used it, but Back In The Day (late 90s), I used f2cl to generate some quite acceptable CL code from Fortran libraries. These days, though, CFFI is so good that it's relatively easy to use any C library. I've recently been using both sqlite and HDF5 from SBCL, and it works pretty well.

I'm trying to find something both fast enough and expressive enough that I can run, say, a sequential monte carlo routine on a complicated model on a ton of data. I'm thinking, as long as it's just me working on this, that CL gives me a bigger lever than C++ does, although C++ is tempting for pure speed.

Strangely enough, given that it's easier to link languages like python, CL, and Lua to C libraries than to C++ libraries, I've been leaning more towards C for little (fast) reusable libraries. To get a bit more expressive power, I actually have been using m4-preprocessed C, which lets me get some of the templatey goodness of C++. But that's probably veering towards madness.
Nov. 2nd, 2007 11:37 am (UTC)
I have, on occasion, used CL (or, rather, a CL subset) and some nasty macrology to generate code in other languages. One of my back-bruner projects generates GLSL from a subset, even paying attention to declared type information (no type inference, yet).
Nov. 3rd, 2007 05:32 am (UTC)
I'm busy pretending to be a statistician/econometrist, so I'm using CL to take a model spec and generate SAS code to fit it to the data, Java code to run it in the current calc framework, actual CL to run it for diagnostics and plots, and will eventually generate C++ for the new calc framework. It's a happy thing.
Nov. 1st, 2007 10:42 am (UTC)
There are people smarter than you? Really?
Nov. 1st, 2007 10:45 am (UTC)
Lots, and by orders of magnitude.
Nov. 3rd, 2007 05:17 am (UTC)
Not orders of magnitude, I don't think, no matter how you count. By achievements --- do you really think there are people who accomplish more every year of their lives than you will in your entire life? By speed of learning --- you've been programming in C++, what, ten years? Do you really think there are people who would be at your level of competency within a month or two? By IQ --- do you really think there's someone out there with a 150000 IQ score?

Maybe if you treat income as a measure of intelligence. By that measure, I'm pretty dumb this year; I've made US$65 so far, I think.
Nov. 3rd, 2007 05:29 am (UTC)
Ok, maybe not orders of magnitude. But there are still lots of them. :)

(I actually learned a bit of C++ back in the fall of 2002, put it down and didn't touch it again until late fall of 2003, and didn't start using it on a weekly basis until ... hm, spring 2004, I think.)
Nov. 1st, 2007 04:34 pm (UTC)
Kanze. Meyers. Alexandrescu. Stepanov. Comeau. Plauger. Josuttis. Vandevoorde. Lee. Gamma. Just to name ten people off the top of my head who are well-known in the C++ community, who are far, far smarter than any C++ hacker I know.

I am the best C++ programmer of anyone I directly know. However, in the grand hierarchy of things, I am a rank novice compared to, say, Alexandrescu, and Alexandrescu is only barely worthy to apprentice to Stepanov.

If anyone ever thinks they're approaching the top of their field, that's a sign to look a little higher. Like the event horizon of a black hole, no matter how close you are to it, it always appears the same distance away. :)
Nov. 1st, 2007 01:40 pm (UTC)
OK, first, hooray for functional programming and first-class functions!

But this monologue points out my biggest beef with C++. The disease runs so deep that there's no way to save the patient. The point of being able to do things like pass around, construct and curry functions is that doing so is powerful and should be easy. Instead, C++ and the STL make is fraught with opportunities for disaster, and full of inexplicable compiler errors that cover 5 pages just to tell you that you left out a "&".

I have first-hand seen many a foot blown off by very smart people trying to do Very Clever things with STL. It's like giving someone a hammer that has sharp edges and pointy spikes everywhere, and is rigged with explosive charges.
Nov. 1st, 2007 02:29 pm (UTC)
In the STL's defense, this was a two-line error message -- but still rather a non-obvious one. And I wasn't even using STLfilt, which makes template-related errors far, far easier to read.

Here's my problem. I like compiled languages, first-class functions, static typing and polymorphism. C++ gives me all but one of the above. There is one language that has all four ... and it's Haskell. Which has apparently acquired a much larger standard library since the last time I looked at it, so maybe I really should sit down and try to learn it properly this time.
Nov. 1st, 2007 03:13 pm (UTC)
I like compiled languages [..]

Piffle. Would you be ok with an intermediate representation? JITs have come a long way -- in many cases, JITs can produce better code than compilers because they can perform optimizations based on run-time trace behavior.

Tangentially related, I recently read about the Low Level Virtual Machine project, in relation to Mac OS X Leopard. It's a VM infrastructure embedded in Leopard... for graphics optimization. See, there's the problem that different video cards support different instruction sets for their GPUs, and different generations have different acceleration support. Instead of falling back to doing video processing on the CPU, they now run-time compile for the instruction set supported by that GPU.
Nov. 1st, 2007 03:22 pm (UTC)
chanson waxes rhapsodic about LLVM on a regular basis. It's high on my list of things to learn more about soon.
Nov. 1st, 2007 03:25 pm (UTC)
sml/nj fits your bill. Have you ever looked at it? Quite a beautiful language, and has some very aggressive compilers.
Nov. 3rd, 2007 05:30 am (UTC)
In addition to SML/NJ, as cipherpunk points out, there is also OCaml and MLton, which is another SML implementation. I vaguely seem to recall that both SML/NJ and OCaml are often somewhere around 2-3 times as fast as Haskell in the shootout.

(I'm assuming that you're interested in a powerful static type system, like C++ with templates, OCaml, Haskell, and SML/NJ, not dumb static type systems that don't statically type-check parametric polymorphism like Java's.)

I haven't really looked at a number of the other languages in the shootout, like D, Clean, Scala, and Nice, to see if they also offer these features.
Nov. 3rd, 2007 05:41 am (UTC)
Looks like D has templates and Clean has a Hindley-Milner type system, so they probably do what you want too.
Nov. 1st, 2007 03:23 pm (UTC)
I have to defend C++ here, in that I think you are misunderstanding what it's supposed to do.

C++ is not a functional programming language. It was never intended to be a functional programming language. You can make a strong argument that people who try to write predominantly functional code in C++ are missing the point.

The intent of the C++ committee--at least in the early days--was to provide an object-oriented extension to C. Over time, other tools were added, but always with the same end in mind: to extend C, to allow people who are currently using C to leverage their existing skills as they slowly adopt newer techniques and methodologies.

So, yes: function composition, decomposition, etc., ought to be efficient and convenient. Unfortunately, how do you do that while retaining backwards compatibility with C? I don't see any way to do it other than the obnoxious syntax currently in use.

The real disease is not C++'s syntax; the real disease is the C legacy.
Nov. 1st, 2007 04:04 pm (UTC)
You can make a strong argument that people who try to write predominantly functional code in C++ are missing the point.

I have to say, I don't try to write predominantly functional code in C++. One of the things I especially love about Python is that it can be procedural, object-oriented and functional whenever you need it to be; the same mostly holds true for C++, it just isn't as functional.
Nov. 1st, 2007 04:18 pm (UTC)
Yes, and hence, not missing the point. Using C++ as a multifunction toolbox: good. Using C++ as a domain-specific language: pretty stipud.

That said, I am disliking C++ more and more as I grow older. The older I get the more I see the C heritage as a boat anchor around its neck. Five years ago I advocated using C++ as a general replacement for C. I still do that, mostly. Today most of my advocacy is directed elsewhere, though: writing the entire app in Python and then using either SWIG or Boost::Python to explode the bottlenecks by writing native code.

Incidentally, the C legacy is my number one complaint with Java and C#, too, so at least I'm consistent.

sml/nj, prolog and Python are the Platonic ideals of syntax, as far as I'm concerned.
Nov. 1st, 2007 04:46 pm (UTC)
I think we're mostly in agreement. My point is that the disease goes all the way back to the C legacy, and instead of trying to create a Frankenstein's monster on top of that, it should just be jettisoned.

Adding features to C(++) might seem like a good idea, but really it's just abuse. Stop hurting yourself.
Nov. 1st, 2007 05:47 pm (UTC)
This sounds superficially like a great idea, until you consider how much C there is in the Real World. Adding modern features to C (OO, functional, templates, etc.) and providing a clean upgrade path is a Big Win for people with code bases measured in MLOC.

I am not a fan of C or C++ for new systems development, but for existing codebases the C-->C++ migration path is hard to beat.
Nov. 1st, 2007 06:06 pm (UTC)
Meh. I would argue that C++'s syntactic similarity to C is actually the biggest danger when taking the C-->C++ migration path, and is responsible for a lot of the bad C++ out there. Encapsulate your legacy code as linkable objects with an abstraction barrier, and refactor into your new language when appropriate. But, I don't really feel like arguing.
Nov. 1st, 2007 01:52 pm (UTC)
You might be interested to know that F# (more or less, the fusion of OCaml and the .NET Framework) will be a first-class .NET citizen in the future. See http://blogs.msdn.com/somasegar/archive/2007/10/17/f-a-functional-programming-language.aspx.
Nov. 1st, 2007 01:57 pm (UTC)
Man, as much as I want to, I just can't get my head around OCaml. :( That is cool news, though.
Nov. 3rd, 2007 05:38 am (UTC)
That's interesting --- what is it in particular you have a hard time with? There are bits of the type-checking that I don't understand, but I have rarely run into them (in the few hundred lines of OCaml I've written).
Nov. 3rd, 2007 07:48 pm (UTC)
Well, for context, I've really only touched OCaml once, in an AI course which handed me my ass. The semester project had to be written in OCaml, and while I got it to work (with extensive help from cloakedwraith), I'm still not exactly sure how. This was back during the spring of 2004, when I still had my programmer training wheels on and didn't really grok things like currying, so I might have a better time with it now. Still, if memory serves, I had a terrible time with the object syntax especially.
Nov. 1st, 2007 02:18 pm (UTC)
"Hmm. I've been asked, in the past, to write articles for programming magazines about projects I've worked on. Perhaps something less long-winded than this diatribe is in order."

You really should. This was fun to read.
Nov. 1st, 2007 03:34 pm (UTC)
A couple of factual corrections:
  • Smartpointers are not quite the bee's knees. A good garbage collector will give better performance than smartpointers, for some reasons Beyond The Scope Of This Reply. Smartpointers are most useful for preserving raii behaviors, but unfortunately you can't really have raii and gc coexisting. (The reason for this is left as an exercise for the reader.)
  • Smartpointers have been in the language since it was first standardized in 1998. std::auto_ptr is a smartpointer and was added to the language to make raii more accessible.
Nov. 1st, 2007 03:40 pm (UTC)
Also, I believe containers of references are explicitly disallowed under the Standard. The reasoning is that you cannot dynamically create a copy of a reference. In Java, Python, etc., a reference is just a pointer with restricted semantics. As such, there's an actual value associated with the reference, and that value can be passed around back and forth.

In C++, a reference is just an alias to another variable. There is no pointer hidden behind the scenes. Since there's no pointer, there's no value associated, which means there's nothing to copy, which means most of the stl falls down goes boom.

I could be mistaken on this, though. You're the one with the Standard sitting on your desk, and I have nothing more reliable than my occasionally spotty memory.
Nov. 1st, 2007 04:05 pm (UTC)
Also, I believe containers of references are explicitly disallowed under the Standard.

Which is why I said "an STL container can contain anything you want, except references." :P
Nov. 1st, 2007 04:13 pm (UTC)
Ah, I skipped the footnotes; apologies. :)
Nov. 1st, 2007 05:04 pm (UTC)
An excellent example of how little I actually progressed into becoming a programmer. Now all I do is make sense of someone else's (or really about 150 someones) C code, which means that I'm really not getting exposed to any of today's useful coding techniques. I still sit around poking at tables of function pointers, thinking "hey, these are kind of neat tricks."
Jun. 14th, 2010 09:05 am (UTC)
also possible
You can also use

find_if(v.begin(), v.end(), !bind(&X::foo, _1))

at least with boost it works.
Jun. 14th, 2010 09:12 am (UTC)
Re: also possible
Good to know - that's even shorter! Thanks.
( 38 comments — Leave a comment )

Latest Month

July 2015


Powered by LiveJournal.com
Designed by Tiffany Chow