C++ Stockholm Syndrome


  • Banned

    @topspin said in C++ Stockholm Syndrome:

    If traits duplicate that functionality, why can't you use traits to achieve the same thing?

    I've spent last two years thinking about possible implemetation of class inheritance using traits. The main pain points are multiple-level hierarchies (required by trait specialization, which only recently hit Rust nightly, and is kinda buggy at the moment), fields (you'd need getters and setters for every field and implement it all from scratch for every type in hierarchy), protected access (impossible, but we can just make everything public), access to all types within currently built and all upstream crates from inside type-creating macro (workaround would be a preprocessor instead of macros, sidestepping the whole issue but having to reimplement a chunk of compiler; also figure out a way to store arbitrary metadata in crates which you could then access from the outside). In short, classes aren't traits, which makes pretend-classes very hard to make with traits.

    @topspin said in C++ Stockholm Syndrome:

    And in what way is a GUI example of "Widget <- TextBox <- RichTextBox" in any way special that GUIs are the only thing that don't work. The same kind of subtyping is used in a lot of places.

    Most class hierarchies can be rewritten as a bunch of pure interfaces and composite objects implementing strategy pattern. GUI is special because it has very very long hierarchies (WPF textbox is 4th descendant of UIElement, which itself is 3rd descendant of DispatcherObject) where each level implements some of the functionality of leaf objects, and particular subclasses do all (or most) of the following: override base methods, leave base methods intact, read and modify base fields, call base implementations of methods they have overridden, provide unused protected submethods with sole purpose of being useful to further subclasses, declare abstract methods for further subclasses to implement and use them extensively in own methods. Usually, any given hierarchy only makes use of two or three of these features at once, and they're no more than 3 levels deep. Also, GUI makes much less use of abstract methods than average.


  • Banned

    @masonwheeler said in C++ Stockholm Syndrome:

    @gąska said in C++ Stockholm Syndrome:

    Okay, maybe not completely incompatible, but incompatible enough (especially in the other way) that it would result in split ecosystem.

    What other way?

    While classes can (probably) implement traits, traits can't derive classes.


  • BINNED

    @gąska That's disappointing. Sounds like (purely from description as I obviously have not tried it yet) Rust will be much less useful than I had hoped for.


  • Banned

    @topspin for GUI. It's awesome for everything else.

    You can always make GUI in more suited language and bind your buttons to Rust calls. There are several libraries for that, although I never used any so can't vouch for their quality.



  • @lb_ said in C++ Stockholm Syndrome:

    @bulb said in C++ Stockholm Syndrome:

    The Results are always there and insist on being handled, so while they make it a bit harder to prototype, they improve the final quality.

    I've always wondered how this works - do the error types have as much variety as C++ and Java? I'm mainly concerned about problems raised by this quote:

    Because we only have one bit of information (the operation failed or not), G cannot handle the error; typically it doesn’t know what is the error. So the only thing that it can do is return false. In general, this approach leads to a situation in which if something bad happens in the system, everybody returns false, and nobody knows what happen. This can have negative consequences on your software. If some small part doesn't work, even if the error can be ignored, it can make your whole software not work. You need to be very careful to avoid these situations.

    Yes, the error types have as much variety as in C++ and Java for the expected-to-be-handled errors (for logic errors you just get panic). Some extra work needs to be done for wrapping them though. Decent solution is still being evaluated.

    @bulb said in C++ Stockholm Syndrome:

    So why are you saying the Rust prospect is no good?

    https://what.thedailywtf.com/topic/20945/why-he-s-dropping-rust (this is the main one I remember scaring me off from Rust)

    Ok, GUI frameworks are one of very few cases where deep inheritance hierarchies actually make sense, which does make them harder in Rust. Still I think it wouldn't be as hard as he presents given approach that is better match for the language—which does show some change in thinking is needed.

    https://what.thedailywtf.com/topic/22463/rust-s-fatal-flaw (though this seems to just be cosmetic, it surprises me that not much care was put into naming things)

    This is cosmetic (and it was an April fool's joke anyway). Also:

    • There are reasons for them: str is a primitive type, so it is lowercase, while library types are uppercase (C# and Java are precedents here), [T] is a primitive too (slight change from C++, C# and Java's T[] to simplify parsing), while the rest is library types. So it just leaves the inconsistency between OsString and PathBuf.
    • In C++ you've got string as owned version of const char * and vector<T> as owned version of T[]? Is that any more consistent?

    A Snake's Tale does give me some hope for Rust yet, at least.

    Yeah, a good language takes some time to design and develop. For example D (the previous promising candidate for C++ replacement, even promoted by some C++ gurus like Andrei Alexandrescu) never made it.


  • Discourse touched me in a no-no place

    @bulb said in C++ Stockholm Syndrome:

    vector<T> as owned version of T[]?

    That'd be array<T>; vector<T> gains more abilities as well (such as being able to be dynamically resized).



  • @topspin said in C++ Stockholm Syndrome:

    @gąska I understand that people use inheritance way too often when they shouldn't (the "composition over inheritance" argument), but why not allow it all? That seems like a distinctly missing feature.

    It conflicts with having open set of implemented interfaces (traits). With open set of interfaces, there are multiple different virtual method tables for each type, which makes downcasting extremely complicated.

    Note that Go does not have inheritance either. It does have (automatic) delegation to member though. Rust is working on the right design for similar (but explicit) delegation. That should cover many use-cases.


  • Discourse touched me in a no-no place

    @bulb Delegation-to-component works pretty well as a pattern, and is useful in building UIs. Yes, the components themselves can be horrible inheritance things in some cases, but the UI that you build with them is usually best done through composition and delegation.



  • @dkf said in C++ Stockholm Syndrome:

    @bulb said in C++ Stockholm Syndrome:

    vector<T> as owned version of T[]?

    That'd be array<T>; vector<T> gains more abilities as well (such as being able to be dynamically resized).

    Rust's Vec<T> is exact equivalent of C++'s vector<T> and Rust's [T] is exact equivalent of C++'s T[]. There can't yet be Rust equivalent of array<T> (because value generic parameters are not implemented yet), but it is a workaround for the auto-decay in C++ that Rust does not do, so it does not need it.



  • @dkf Yes. So when the support for delegation gets in, UI won't be much harder in Rust then elsewhere.


  • Banned

    @bulb said in C++ Stockholm Syndrome:

    Rust's Vec<T> is exact equivalent of C++'s vector<T> and Rust's [T] is exact equivalent of C++'s T[].

    Except Rust's [T] has lots and lots of very useful methods for array operations (e.g. iterators or sorting), and most of Vec<T>'s functionality is done via dereference to [T]. Honestly, [T] is much closer to array<T,N> except for statically-generic size (it's dynamically-generic).


  • Impossible Mission - B

    @bulb said in C++ Stockholm Syndrome:

    It conflicts with having open set of implemented interfaces (traits). With open set of interfaces, there are multiple different virtual method tables for each type, which makes downcasting extremely complicated.

    🤷♂ Delphi, Java, and the CLR have no problem with it...


  • Considered Harmful

    @gąska I think you missed a few brackets there.


  • Considered Harmful

    @masonwheeler Yes, and they have big runtimes with bytecode. Rust is very bare-metal, as much as possible. IIRC trait objects were controversial when they were introduced.


  • Banned

    @pie_flavor said in C++ Stockholm Syndrome:

    @gąska I think you missed a few brackets there.

    Fixed.



  • @masonwheeler said in C++ Stockholm Syndrome:

    @bulb said in C++ Stockholm Syndrome:

    It conflicts with having open set of implemented interfaces (traits). With open set of interfaces, there are multiple different virtual method tables for each type, which makes downcasting extremely complicated.

    🤷♂ Delphi, Java, and the CLR have no problem with it...

    Neither of them has it, so obviously they don't.

    Open set of interfaces means that you can define an interface and define how existing types from other libraries implement it and suddenly objects of those types can be handled via references to the new interface. Both Rust and Go can do that (in Rust you explicitly say type X implements trait Y this way, in Go every type automatically implements interface it has methods of matching signatures for and you can define more methods for already defined type). The only other language I know of that can do this is Haskell and that has no inheritance either.

    C# extension methods come closest, but they can only add methods to existing interfaces, not create new ones. As far as I can tell, Java has nothing in that direction and Scala tries to by auto-generating adaptors in what ends up being a very leaky abstraction. I haven't seen Delphi for 20 years, but I don't think it has anything more than Java.



  • @bulb said in C++ Stockholm Syndrome:

    Open set of interfaces means that you can define an interface and define how existing types from other libraries implement it and suddenly objects of those types can be handled via references to the new interface. Both Rust and Go can do that (in Rust you explicitly say type X implements trait Y this way, in Go every type automatically implements interface it has methods of matching signatures for and you can define more methods for already defined type). The only other language I know of that can do this is Haskell and that has no inheritance either.

    Albeit not a language feature, you can achieve this in C++ via type erasure. It's not cost free (at least if you want to do it in runtime via a run-time interface type; if you do it in compile time, you don't need to erase the type and there's no overhead).

    This blag explains the details, but essentially with some plumbing you can arrive at (example more or less from the blag):

    void open_door_and_greet(OpenerAndGreeter const &g, char const* name) {
        g.open();
        g.greet(name);
    }
    

    Essentially, this accepts any object that implements compatible open() and greet() methods as the argument g. The overhead in the demonstrated implementation is quite high (alloc + virtual calls), but a slightly more clever implementation can get rid of the alloc in most cases (I think). Note that there are no templates involved in the function definition -- it really is a normal function.

    You could of course restrict this to only accept types are explicit state that the implement the OpenerAndGreeter interface, and you can also do that for external types whose definition you can't change.

    Concepts (probably C++20) will make the plumbing a bit more convenient too, I imagine.


  • Discourse touched me in a no-no place

    @cvi said in C++ Stockholm Syndrome:

    Note that there are no templates involved in the function definition -- it really is a normal function.

    That's because there's effectively a delegate inserted between the original object(s) that will be receiving those method calls and the object being passed into that method. Which is clever, but has exactly the same overhead at runtime that any other delegation system would have.

    Also, the classes used to set this all up currently make my head spin and stomach churn; the definitions involved are non-trivial.



  • @dkf said in C++ Stockholm Syndrome:

    Which is clever, but has exactly the same overhead at runtime that any other delegation system would have.

    Sure. I'm wondering how Go or Rust implement it under the hood. Do they autogenerate a delegate too? You can do a bunch of tricks to make it cheaper (i.e., the delegate can just be some static code, but that should be possible using an implementation in C++ too if you mess with it for a while). Instinctively, I don't see how you can do this without any glue code at all, but who knows (unless you emit specialized versions of the callee)?

    Also, the classes used to set this all up currently make my head spin and stomach churn; the definitions involved are non-trivial.

    You can (probably) hide a bunch of the boilerplate in a library or similar; if I were to implement this, I'd definitively be looking for a way to make the delegate-definitions simpler. Pretty sure I've seen implementations of that already, but this is not something I use currently, so I've not kept track of those.

    The blag I linked is more about how one would implement this from scratch, meaning it doesn't try to hide the ugly parts



  • @bulb said in C++ Stockholm Syndrome:

    @masonwheeler said in C++ Stockholm Syndrome:

    @bulb said in C++ Stockholm Syndrome:

    It conflicts with having open set of implemented interfaces (traits). With open set of interfaces, there are multiple different virtual method tables for each type, which makes downcasting extremely complicated.

    🤷♂ Delphi, Java, and the CLR have no problem with it...

    Neither of them has it, so obviously they don't.

    Open set of interfaces means that you can define an interface and define how existing types from other libraries implement it and suddenly objects of those types can be handled via references to the new interface. Both Rust and Go can do that (in Rust you explicitly say type X implements trait Y this way, in Go every type automatically implements interface it has methods of matching signatures for and you can define more methods for already defined type). The only other language I know of that can do this is Haskell and that has no inheritance either.

    C# extension methods come closest, but they can only add methods to existing interfaces, not create new ones. As far as I can tell, Java has nothing in that direction and Scala tries to by auto-generating adaptors in what ends up being a very leaky abstraction. I haven't seen Delphi for 20 years, but I don't think it has anything more than Java.

    It's not quite Java, but kotlin has extension functions and fields, but there is a bit missing in there to do what you describe. And it also lacks a bit compared to for instance Swift (in that in swift you can add extensions to make a class fulfill an interface, but last I worked in kotlin this was not possible)


  • Discourse touched me in a no-no place

    @cvi said in C++ Stockholm Syndrome:

    Do they autogenerate a delegate too?

    No idea; I don't use either of them. :)

    I know some languages don't have the overhead per se (e.g., Python) but they do that by forcing all method calls through the dynamic path. That sounds initially like it is all bad, but it allows for other capabilities to be enabled fairly easily. It's the classic static-vs-dynamic tradeoff; static gets you speed, but dynamic gets you flexibility.



  • @pie_flavor said in C++ Stockholm Syndrome:

    @masonwheeler Yes, and they have big runtimes with bytecode. Rust is very bare-metal, as much as possible. IIRC trait objects were controversial when they were introduced.

    .NET uses MSIL as an intermediate, but everything is compiled to native code prior to execution. Zero bytecode at runtime.


  • Java Dev

    @cvi said in C++ Stockholm Syndrome:

    I'm wondering how Go or Rust implement it under the hood.

    As I understand it, in rust, if you pass an object to a function accepting a trait, you actually pass 2 pointers: A pointer to the object instance, and a separate pointer to the vtable for that trait on that class. The caller either knows the class at link time, or similarly gets the vtable passed in.


  • Banned

    @cvi said in C++ Stockholm Syndrome:

    I'm wondering how Go or Rust implement it under the hood.

    Don't know about Go, but Rust achieves this by attaching vtable to the pointer rather than to the object (so-called "fat pointers" - implemented as tuple of pointer to object and pointer to vtable, but on syntax level they behave like regular pointers). This way, you can add additional traits to a type without altering layout of the object itself, and the method call is no more expensive than a method call in classic class hierarchy. No adapters, no additional indirections. The only added cost is doubled size of pointers (and only pointers to traits - pointers to concrete types don't have any vtable, so they have their regular size). Also, it only applies to dynamic dispatch - if you use static dispatch (a matter of changing your function's declaration; no need to modify the trait or the type itself), it becomes truly zero cost.


  • Banned

    @thecpuwizard said in C++ Stockholm Syndrome:

    @pie_flavor said in C++ Stockholm Syndrome:

    @masonwheeler Yes, and they have big runtimes with bytecode. Rust is very bare-metal, as much as possible. IIRC trait objects were controversial when they were introduced.

    .NET uses MSIL as an intermediate, but everything is compiled to native code prior to execution. Zero bytecode at runtime.

    The generated native code is usually very bloated to accomodate all the dynamism of bytecode.



  • @cvi said in C++ Stockholm Syndrome:

    Albeit not a language feature, you can achieve this in C++ via type erasure.

    You can do anything in C++, because you can create the low-level implementation yourself.

    @gąska said in C++ Stockholm Syndrome:

    @cvi said in C++ Stockholm Syndrome:

    I'm wondering how Go or Rust implement it under the hood.

    Don't know about Go, but Rust achieves this by attaching vtable to the pointer rather than to the object (so-called "fat pointers" - implemented as tuple of pointer to object and pointer to vtable, but on syntax level they behave like regular pointers). This way, you can add additional traits to a type without altering layout of the object itself, and the method call is no more expensive than a method call in classic class hierarchy. No adapters, no additional indirections. The only added cost is doubled size of pointers (and only pointers to traits - pointers to concrete types don't have any vtable, so they have their regular size). Also, it only applies to dynamic dispatch - if you use static dispatch (a matter of changing your function's declaration; no need to modify the trait or the type itself), it becomes truly zero cost.

    It is exactly the same in Go. Except Go does not have generics, so it always ends up with dynamic dispatch when you use interfaces.


  • :belt_onion:

    @gąska said in C++ Stockholm Syndrome:

    The generated native code is usually very bloated to accomodate all the dynamism of bytecode.

    Citation needed; that sounds like bullshit to me. (The generated native code doesn't need to accommodate the dynamism of shit, it just needs to accommodate what it's generating right here, right now.)


  • Discourse touched me in a no-no place

    @bulb said in C++ Stockholm Syndrome:

    You can do anything in C++, because you can create the low-level implementation yourself.

    … Chiselling it out of raw assembly code if necessary. Some things in C++ come under the heading of “Sure, you can do that, but why would you willingly work that hard?”

    The silly thing is our own codebase is a bit that way (except in C for various unimportant — for this discussion — reasons). We do build some pieces out of raw assembly code, but that's the sane thing to do for low level interrupt handlers (there's a few bits that can't be expressed in ordinary C at all) and the absolute innermost loop of the most performance critical simulation loop (where every instruction costs). Our system is a weird hybrid of embedded and supercomputing. ;)


  • Banned

    @heterodox it has at the very least null-check everything that's not provably non-null. I doubt JITs are doing whole-program-optimizations as they're rather expensive, and without them you cannot elide most null checks. Inlining virtual calls would also be pretty hard (unless modern JITs generate separate code for every concrete type; it certainly wasn't the case a few years back, but it might be now).



  • @dkf said in C++ Stockholm Syndrome:

    … Chiselling it out of raw assembly code if necessary. Some things in C++ come under the heading of “Sure, you can do that, but why would you willingly work that hard?”

    Which applies to Rust as well (with help of the unsafe keyword so it lets you touch unchecked “raw” pointers).



  • @gąska said in C++ Stockholm Syndrome:

    @thecpuwizard said in C++ Stockholm Syndrome:

    @pie_flavor said in C++ Stockholm Syndrome:

    @masonwheeler Yes, and they have big runtimes with bytecode. Rust is very bare-metal, as much as possible. IIRC trait objects were controversial when they were introduced.

    .NET uses MSIL as an intermediate, but everything is compiled to native code prior to execution. Zero bytecode at runtime.

    The generated native code is usually very bloated to accomodate all the dynamism of bytecode.

    There is a significant amount of generated DATA, but the code [resulting from JIT] is quite good. I spend a fair amount of time working at the native level.

    It is key to remember that just having a debugger attached to the process disables most of the JIT optimizations, so one needs to be very careful when analyzing the native implementations.


  • Banned

    @thecpuwizard how good are modern JITs at optimizing null checks? 10%, 50%, 90%?



  • @gąska said in C++ Stockholm Syndrome:

    @thecpuwizard how good are modern JITs at optimizing null checks? 10%, 50%, 90%?

    The answer is "it depends on many things". The first part is the necessity (at the JIT level) for even doing a null check! After all, an attempt to dereference null [talking .NET specifically] will simply result in an exception being thrown; on the "happy path" (non-null) there is no explicit check needed.

    One of the things JIT (again talking .NET) is great at is inlining! [remember this is disabled if there is a debugger attached!]. This often eliminates the double referencing associated with pointer/reference types and around virtual methods. If the "final location" of the code is in a context where the concrete type is known (or at least the method being invoked is sealed) the appropriate code is generated to call directly, even though the C# source may have been written taking an interface in the context where none of the concrete classes were known.

    As a side note: I have a standing bet. Given a fixed performance criteria, if someone can write C++ code that is guaranteed to meet that performance metric on a Windows box, then I can write .NET (typically C#) code that meets the same spec. I have not had to pay out yet (and while some people who have paid up, there are many more who "owe" me).


  • Banned

    @thecpuwizard said in C++ Stockholm Syndrome:

    @gąska said in C++ Stockholm Syndrome:

    @thecpuwizard how good are modern JITs at optimizing null checks? 10%, 50%, 90%?

    The answer is "it depends on many things". The first part is the necessity (at the JIT level) for even doing a null check! After all, an attempt to dereference null [talking .NET specifically] will simply result in an exception being thrown; on the "happy path" (non-null) there is no explicit check needed.

    Yeah, I totally forgot that on Windows a segfault is SEH. It's much harder (though technically not entirely impossible) to handle it cleanly on POSIXes.


  • Discourse touched me in a no-no place

    @gąska said in C++ Stockholm Syndrome:

    how good are modern JITs at optimizing null checks?

    They're extremely good; as soon as the code within a function is past the point at which a null would have caused problems, the compiler can aggressively drop null checks. Rust will also be picking up on the same sort of technology as well; I know that LLVM does that sort of thing (and related; almost any boolean check in the low-level code can be propagated) and that it can help a lot with cutting out useless code. (I don't know whether it can propagate the non-null-ness across function calls reliably; we do that class of reasoning in our front-end before it gets to the LLVM level so we don't really care ourselves.)


  • Banned

    @dkf the difference between Rust and, say, Java is that in Rust, there is no null. All pointers are non-nullable unless wrapped in Option. So it's not as important as in other languages.



  • @gąska said in C++ Stockholm Syndrome:

    The generated native code is usually very bloated to accomodate all the dynamism of bytecode.

    No it's not.


  • Banned

    @blakeyrat I know.



  • @gąska Oh I guess it was the other gaska who typed that post then.


  • Discourse touched me in a no-no place

    @gąska said in C++ Stockholm Syndrome:

    All pointers are non-nullable unless wrapped in Option.

    You're thinking at the wrong level. To the optimiser, those are actually equivalently difficult problems.


  • Banned

    @dkf said in C++ Stockholm Syndrome:

    @gąska said in C++ Stockholm Syndrome:

    All pointers are non-nullable unless wrapped in Option.

    You're thinking at the wrong level. To the optimiser, those are actually equivalently difficult problems.

    If 99% if pointers are explicitly marked as never being null, it really changes the game.


  • Discourse touched me in a no-no place

    @gąska said in C++ Stockholm Syndrome:

    If 99% if pointers are explicitly marked as never being null, it really changes the game.

    No. It doesn't. Most of the pointers that you find in modern C++ are also non-null, for example, as they're the implementation form of references.


  • Banned

    @blakeyrat said in C++ Stockholm Syndrome:

    @gąska Oh I guess it was the other gaska who typed that post then.

    It was. The @Gąska who wrote it is the one from 3 hours ago who didn't have a chance to read the following posts from @TheCPUWizard and @dkf who explained thoroughly why he was wrong.



  • @gąska said in C++ Stockholm Syndrome:

    It was. The @Gąska who wrote it is the one from 3 hours ago who didn't have a chance to read the following posts from @TheCPUWizard and @dkf who explained thoroughly why he was wrong.

    But he wrote it with such certainty! That other gaska must have a malfunctioning brain if he'd state something that's at best on extremely shaky factual ground with absolutely no hint that he might not know what the hell he's talking about. You should go find him and beat him up for making random people on this forum, like me, think you are a total idiot. Send him a black-eye message that, hey, it's not ok to just make up shit and post it as if it were fact.


  • Banned

    @dkf said in C++ Stockholm Syndrome:

    @gąska said in C++ Stockholm Syndrome:

    If 99% if pointers are explicitly marked as never being null, it really changes the game.

    No. It doesn't. Most of the pointers that you find in modern C++ are also non-null, for example, as they're the implementation form of references.

    I thought we're talking about C#? Also, C++'s smart and raw pointers are nullable, Rust's aren't.


  • Banned

    @blakeyrat said in C++ Stockholm Syndrome:

    @gąska said in C++ Stockholm Syndrome:

    It was. The @Gąska who wrote it is the one from 3 hours ago who didn't have a chance to read the following posts from @TheCPUWizard and @dkf who explained thoroughly why he was wrong.

    But he wrote it with such certainty!

    OMG someone was wrong and made fool out of themselves! Unheard of!

    That other gaska must have a malfunctioning brain if he'd state something that's at best on extremely shaky factual ground with absolutely no hint that he might not know what the hell he's talking about.

    Reminds me of someone. And their username doesn't contain any diacritics.


  • Discourse touched me in a no-no place

    @gąska said in C++ Stockholm Syndrome:

    Also, C++'s smart and raw pointers are nullable, Rust's aren't.

    So? Most C++ programmers use references, if not everywhere in many places, and those are definitely not null, whereas I think most don't use smart pointers (I don't think I've ever written anything that used those). Yet in C++ implementations, references definitely convert into non-null pointers under the hood.

    You're making all this out to be a bigger deal than it is.


  • Discourse touched me in a no-no place

    @gąska said in C++ Stockholm Syndrome:

    I thought we're talking about C#?

    Speak for yourself; I wasn't. 😈


  • Banned

    @dkf said in C++ Stockholm Syndrome:

    @gąska said in C++ Stockholm Syndrome:

    Also, C++'s smart and raw pointers are nullable, Rust's aren't.

    So? Most C++ programmers use references, if not everywhere in many places, and those are definitely not null, whereas I think most don't use smart pointers (I don't think I've ever written anything that used those).

    High-level C++ is full of smart pointers. Especially shared_ptr. And low-level is full of raw pointers. Although it doesn't really matter since C++ sidesteps the whole issue by making null dereference UB.


  • Discourse touched me in a no-no place

    @gąska said in C++ Stockholm Syndrome:

    High-level C++ is full of smart pointers.

    :sideways_owl:

    I really don't recall seeing lots of code that uses them. Maybe the tools cow-orkers you were working with were keen on using them, but they're really nothing like as commonplace as plain old references.


Log in to reply