OOP is dead


  • Discourse touched me in a no-no place

    @DogsB said in OOP is dead:

    Maybe with the people you work with.

    My team seem to be able to cope with internal vs external and that's about it. 😒



  • @Steve_The_Cynic said in OOP is dead:

    You say that, except that I've seen the domination message in a hierarchy with no virtual inheritance (but interface-style multiple inheritance), and it was virtual functions called via one of the base classes.

    For what it's worth, I checked what I wrote (by modifying the example posted above) with VS2019 before posting. I'm pretty sure this is the behavior currently mandated by the standard; but might be my test case was too simplistic to trigger any "interesting" case, where name hiding resolves the ambiguity in a surprising way. That said, strictly according to the standard, two member functions just having the same signature do not make them the same function, and if name lookup finds them both your call is ambiguous; that's always been the case.

    @Steve_The_Cynic said in OOP is dead:

    This was C++98 compiled with Visual C++6, so it may have been a Microsoft bug, of course.

    That may have something to do with it. AFAIR earlier VC++ versions had some "creative" interpretations of the standard, and it might even be that the standard behavior has been made stricter in the non-ancient revisions. I do have vague memories of having encountered some surprises with VC++6 in this context.



  • @dkf said in OOP is dead:

    There are far more different ways of doing OO than most developers seem to think. I tend to think that until people have written two or three of them, they don't really know what they're doing on the topic; there are some really complex design decisions that you can't really understand until after you've made the key mistakes involved. 😜

    I think people should at least try CLOS - if nothing else, it shows that even some universally held concepts are not needed. In particular, the assumption that method belongs to the class. Whenever I ask (in these discussions about OOP) why should be the method polymorphism limited to single class hierarchy, people just stare at me.



  • @dfdub said in OOP is dead:

    Ignore the default value for a second. From an interface perspective, what would be more reasonable as the return value of the member function? Do you want to copy the whole string every time someone queries a configuration value?
    The configuration file object is the obvious owner of the configuration values, so the fact that the returned references will only be valid as long as the configuration object should be pretty obvious to the caller.

    To make it clear up-front: I don't disagree with this. I think returning a reference is perfectly good in this context (minus the potential problems with the default parameter already pointed out).

    However, returning a copy of a string is probably also alright. If you're dealing with strings and/or configuration values, you're probably off the hot path already, so that probably won't murder performance. Since it's configuration values, you're probably not doing it millions of times either nor are the strings likely to be 100s of megabytes in size.

    (I occasionally need to remind myself of these things as well.)


  • kills Dumbledore

    @topspin said in OOP is dead:

    That wasn't my point though. If you're a freshman struggling to understand the concept of a pointer, fine, but an actual developer? Bad sign.

    I never really got why people found pointers so difficult to understand. Sure, the dereferencing can get a bit complex when you've got a pointer to a pointer to an array of pointers, but that's complexity, not a difficult concept



  • @Jaloopa said in OOP is dead:

    I never really got why people found pointers so difficult to understand. Sure, the dereferencing can get a bit complex when you've got a pointer to a pointer to an array of pointers, but that's complexity, not a difficult concept

    Me either. It's one of those thing some people seem to be eternally scared off. But from my experience teaching, pointers don't actually cause that much problems. In fact, all the screaming about how terrible and "difficult" pointers are probably causes more problems than the concept of pointers itself.


  • Discourse touched me in a no-no place

    @cvi said in OOP is dead:

    Since it's configuration values, you're probably not doing it millions of times either nor are the strings likely to be 100s of megabytes in size.

    If you're configuring things millions of times per run with strings of 100MB or more, you have quite a lot more problems too.


  • Discourse touched me in a no-no place

    @Jaloopa said in OOP is dead:

    I never really got why people found pointers so difficult to understand. Sure, the dereferencing can get a bit complex when you've got a pointer to a pointer to an array of pointers, but that's complexity, not a difficult concept

    Pointers aren't complicated… but they do raise the complexity exponentially (and references are similar in this regard). The first few levels don't make a big numerical difference to the complexity, but the more levels there are, the more brain-bending they become. I've seen and worked with three levels of dereference without too much difficulty, but when you encounter code that's working at five levels… you're definitely wondering WTF is going on, especially as that usually means that at least one of the levels involved is doing something funky with dynamically changing which variables are being updated from deep calls.



  • @Jaloopa said in OOP is dead:

    I never really got why people found pointers so difficult to understand

    The concept is simple but it very quickly descends into a can of worms.

    If I'm dealing with an argument of type int, then I know what I can do with that. It's a number, it isn't going to change (unless I re-assign it) and I can pass it to other bits of code or primitives with simple, easily understood results.

    If I'm passed an argument of type int*, I can probably read it and get a number. But if I read it on the next line, I might get a different number, because some concurrent code might have modified it. I might get a memory access violation if the code that generated the pointer is no longer in scope and it cleaned up the memory. (Or I might just get a random meaningless number if the memory is re-used in process.) If I pass it to another piece of code, the number might be changed.

    If I generate a pointer result type, who is responsible for allocating the memory into which the actual value is written? Who is responsible for cleaning it up? When should that happen?

    Yes, it's a simple context - you have an address of where some data is stored - but that decoupling from the actual data means all sorts of bad and confusing things can happen.

    There's a reason that modern high level languages don't give you pointers by default, and hide a lot of the things you used to need them for behind object reference semantics and garbage collection. Even C++ recognises this, hence pointer cover classes in the standard library and languages features like refs that make statements about some of those questions.


  • BINNED

    @bobjanova problems arising from the combination of mutability and concurrency don't change at all when you go from pointers to references.



  • @topspin said in OOP is dead:

    problems arising from the combination of mutability and concurrency don't change at all when you go from pointers to references.

    Nor do the problems arising from life time considerations. The only real difference between pointers and references is that you can do arithmetic with pointers.



  • @bobjanova said in OOP is dead:

    If I'm passed an argument of type int*, I can probably read it and get a number. But if I read it on the next line, I might get a different number, because some concurrent code might have modified it.

    It won't have changed, not in a legal program in C/C++, anyway. For it to be legal, you would need a synchronization point in-between the reads, or the variable would need to be an atomic. Alternatively, for memory mapped IO stuff, it would need to be a volatile int*. All of those are distinct from having an int* and reading it twice.



  • @topspin Mutability, true - but objects typically provide some guarantees about what might be mutable, so at least the problem is limited in scope. With a pointer you can completely replace whatever it was pointing to without any access control.

    But lifetime no - if you are passed a reference then a high level language will guarantee that reference is valid until it goes out of scope.


  • Considered Harmful

    @cvi said in OOP is dead:

    @bobjanova said in OOP is dead:

    If I'm passed an argument of type int*, I can probably read it and get a number. But if I read it on the next line, I might get a different number, because some concurrent code might have modified it.

    It won't have changed, not in a legal program in C/C++, anyway. For it to be legal, you would need a synchronization point in-between the reads, or the variable would need to be an atomic. Alternatively, for memory mapped IO stuff, it would need to be a volatile int*. All of those are distinct from having an int* and reading it twice.

    Reading this shit makes me so glad I don't have to touch C++.



  • @cvi said in OOP is dead:

    It won't have changed, not in a legal program in C/C++, anyway. For it to be legal, you would need a synchronization point in-between the reads

    Is this a compiler optimisation thing? Pointers shared between threads are referring to memory that can get changed, and so at least in theory you can get an unexpected answer when doing

    int b = *a; (insert code); int c = *a; assert(c == b)

    And yes in many cases a compiler will optimise that away, but that only makes the question more complicated because now I need to know what the optimiser thinks is a 'synchronisation point'.



  • @error Why this specifically? This is a restriction that makes programming easier for the most part -- you're not required to assume that values will change willy-nilly under you.


  • Considered Harmful

    @cvi said in OOP is dead:

    @error Why this specifically? This is a restriction that makes programming easier for the most part -- you're not required to assume that values will change willy-nilly under you.

    📠 :barrier: 🃏 but, really, the fact that the C/C++ spec has this narrowly beaten path of "defined behavior" that you can easily stray from without realizing it and get unexpected nasal demons is a major turn-off. The spec deals with abstract concepts like synchronization points, which have have no tangible representation in the code, but yet the developer needs to know intimately to stay on the "defined behavior" golden path.

    It feels like navigating a minefield, except when I take a wrong step I plant a live mine (bug) in my program for the users to find for me.



  • @bobjanova It's a bit more fundamental than a compiler optimization thing, it's defined in the memory model.

    In the example you have, the assert can indeed trigger, but it depends on the (insert code) part. Depending on what a is pointing to, there doesn't even have to be any concurrency involved, a function called in (insert code) could also be changing it.

    Synchronization points are not defined by what the optimizer thinks, but rather, the optimizer has to know what the standard (library) defines as synchronization points and is only allowed to optimize based on this. But that's still a bit backward. You as a programmer will need to know whether or not a value can change concurrently. If it does, you need to guard against that. Meaning, you will need to ensure that there is a synchronization point (e.g., via a mutex, a barrier, joining threads, etc) - if you don't, the program is not legal.

    Yes, the implication is that if you share memory between threads, and any of the threads can change values in the memory, you will need to protect against memory hazards somehow.

    Mutability, true - but objects typically provide some guarantees about what might be mutable, so at least the problem is limited in scope. With a pointer you can completely replace whatever it was pointing to without any access control.

    Not sure what you mean here. The int example allows you to change the value of the int arbitrarily. If you have an object, you can only do stuff that is legal to do with the object (call its methods, etc), regardless of whether or not it's a pointer or reference. If the object defines a copy-constructor, you can "replace" the object completely, otherwise you can't (shouldn't).


  • Considered Harmful

    @cvi said in OOP is dead:

    otherwise you can't (shouldn't).

    If the language allows it, someone will do it. I'm all for childproofing language features. Even if I know that using a given feature is a Very Bad Idea, I'm going to end up maintaining code written by Little Jimmy who thought it was "super neat and clever" and is very proud of his little trick, that is now integral to the process.



  • @error said in OOP is dead:

    which have have no tangible representation in the code

    Mutexes, barriers, joining threads, etc? Those seem fairly tangible.

    But, yeah, I can get the point about undefined behaviour. I personally still think it's overrated as a problem. IME, the vast majority of bugs/errors aren't related to undefined behaviour, and are mostly bugs that would have appeared elsewhere as well. To me, the languages that bend over backwards to avoid undefined behaviour, try to help me with something that isn't a huge deal, but instead create problems for me that are way harder to deal with in the end.


  • Considered Harmful

    @cvi said in OOP is dead:

    Mutexes, barriers, joining threads, etc? Those seem fairly tangible.

    I was thinking of "sequence points", the lack of which is why x = ++x is UB.


  • Trolleybus Mechanic

    @error said in OOP is dead:

    @cvi said in OOP is dead:

    Mutexes, barriers, joining threads, etc? Those seem fairly tangible.

    I was thinking of "sequence points", the lack of which is why x = ++x is UB.

    Is a race condition really considered undefined behavior? I don't think race conditions are well defined for any language, hence why they're called race conditions.



  • @cvi said in OOP is dead:

    if you don't, the program is not legal.

    I think I might not be understanding what you mean by 'legal'. I assumed you meant 'it won't compile if you do it wrong', but you actually seem to be using it to mean 'written correctly'.

    Which is kind of my point here - it's very easy to write 'illegal' code with pointers, and knowing enough to know whether what you're doing is 'legal' (now and in the future) is really hard. And that's why people are scared of pointers.

    The int example allows you to change the value of the int arbitrarily. If you have an object, you can only do stuff that is legal to do with the object (call its methods, etc), regardless of whether or not it's a pointer or reference

    Ok I guess this needs an example. First, a reprise of my first post about pointers:

    Value types are really simple to use and don't do any scary things to you:

    f(int i) {
      var a = i;
      (stuff)
      var b = i;
      assert(b == a); // Can never fail
    }

    (This can also be a struct in C#, or a non-reference heap class/struct in C, etc, as well as a primitive.)

    Pointers, as I said in my previous post, are scary because almost no matter what (stuff) is, there is some chance that assertion can fail based on things outside the code.

    Let's imagine an object oriented scenario now:

    class X {
     public readonly int y;
     public int z;
    }
    
    f1(by_value X x) {
     var a = x;
     (stuff)
     var b = x;
    assert(a.y == b.y); // [1]
    assert(a.z == b.z); // [2]
    }
    
    f2(by_ref X x) { ... }
    f3(X* x) { ... }
     

    Now, considering the assertions. With a value type x is local to the method and cannot be modified externally*, unless you pass it by reference to some external method in (stuff), which is fairly explicit.

    With the reference version, [2] can fail, just like if it were a pointer, because external forces might change the object, but [1] shouldn't because it's marked immutable by the object spec. And the object identity (whatever that means in your language) is one of those immutable things - though I'm not sure that even has a meaning in a direct memory language to be fair.

    With pointers, they can both fail, because some code elsewhere can do

    *pointer_to_x = new X(new_y, new_z)

    ... and completely overwrite whatever you thought X was, with no respect for X's mutability rules.


  • Considered Harmful

    @mikehurley said in OOP is dead:

    @error said in OOP is dead:

    @cvi said in OOP is dead:

    Mutexes, barriers, joining threads, etc? Those seem fairly tangible.

    I was thinking of "sequence points", the lack of which is why x = ++x is UB.

    Is a race condition really considered undefined behavior? I don't think race conditions are well defined for any language, hence why they're called race conditions.

    This is not technically a race condition. A race condition arises when a behavior is non-deterministic based on timing. This is about the deterministic output of what the compiler produces for a given input, and it's Undefined Behavior specifically because what the output should be based on that input is ambiguous per the spec.


  • Discourse touched me in a no-no place

    @error said in OOP is dead:

    This is about the deterministic output of what the compiler produces for a given input, and it's Undefined Behavior specifically because what the output should be based on that input is ambiguous per the spec.

    In particular, for x = ++x; in C and C++, two conformant compilers can generate different code for the same input. Or two different runs of the same compiler can; the spec allows it. (By contrast, the code is well-defined in C# and Java as those both have a defined evaluation order in their abstract model, but really stupid.)


  • BINNED

    @error said in OOP is dead:

    @cvi said in OOP is dead:

    otherwise you can't (shouldn't).

    If the language allows it, someone will do it. I'm all for childproofing language features. Even if I know that using a given feature is a Very Bad Idea, I'm going to end up maintaining code written by Little Jimmy who thought it was "super neat and clever" and is very proud of his little trick, that is now integral to the process.

    Except that you can't child proof a program. Next you're telling me there's no bugs in JS programs, and everything should run on JS.


  • Trolleybus Mechanic

    @error said in OOP is dead:

    @mikehurley said in OOP is dead:

    @error said in OOP is dead:

    @cvi said in OOP is dead:

    Mutexes, barriers, joining threads, etc? Those seem fairly tangible.

    I was thinking of "sequence points", the lack of which is why x = ++x is UB.

    Is a race condition really considered undefined behavior? I don't think race conditions are well defined for any language, hence why they're called race conditions.

    This is not technically a race condition. A race condition arises when a behavior is non-deterministic based on timing. This is about the deterministic output of what the compiler produces for a given input, and it's Undefined Behavior specifically because what the output should be based on that input is ambiguous per the spec.

    Seems odd that that's considered UB. X has a value, it gets incremented, and the result is stored back into X. What information is missing? I thought the LHS effectively didn't exist while the RHS was being executed.


  • Trolleybus Mechanic

    @mikehurley said in OOP is dead:

    @error said in OOP is dead:

    @mikehurley said in OOP is dead:

    @error said in OOP is dead:

    @cvi said in OOP is dead:

    Mutexes, barriers, joining threads, etc? Those seem fairly tangible.

    I was thinking of "sequence points", the lack of which is why x = ++x is UB.

    Is a race condition really considered undefined behavior? I don't think race conditions are well defined for any language, hence why they're called race conditions.

    This is not technically a race condition. A race condition arises when a behavior is non-deterministic based on timing. This is about the deterministic output of what the compiler produces for a given input, and it's Undefined Behavior specifically because what the output should be based on that input is ambiguous per the spec.

    Seems odd that that's considered UB. X has a value, it gets incremented, and the result is stored back into X. What information is missing? I thought the LHS effectively didn't exist while the RHS was being executed.

    I could understand x = x++ being UB.



  • @bobjanova said in OOP is dead:

    With pointers, they can both fail, because some code elsewhere can do
    *pointer_to_x = new X(new_y, new_z)
    ... and completely overwrite whatever you thought X was, with no respect for X's mutability rules.

    First a minor nitpick: the code you wrote won't compile; you probably meant *pointer_to_x = X(new_x,new_y). The alternative pointer_to_x = new X(...) would replace the pointer value, but leave the original object intact (and other pointers will still refer to it).

    Second: you could try the same with references, e.g. reference_to_x = X(new_x, new_y).

    However, both fail to compile, because you cannot copy-assign objects with const members (the default copy constructors are deleted).

    The more insidious version is reference_to_x.~X(); new (&reference_to_x) X( ... ); , which is much harder to prevent. (It's been subject to at least one defect report for the C++ standard.) However, code like that sets of all sorts of alarm bells anyway.

    Finally, even without the const/readonly problems, your (code) will likely name x explicitly again, in which case, it's obvious that parts of it may change. If it doesn't, it's indicative of a deeper problem in the code, because multiple things end up with mutable references to a single object without making that clear to the programmer.


  • Java Dev

    @dkf said in OOP is dead:

    @Jaloopa said in OOP is dead:

    I never really got why people found pointers so difficult to understand. Sure, the dereferencing can get a bit complex when you've got a pointer to a pointer to an array of pointers, but that's complexity, not a difficult concept

    Pointers aren't complicated… but they do raise the complexity exponentially (and references are similar in this regard). The first few levels don't make a big numerical difference to the complexity, but the more levels there are, the more brain-bending they become. I've seen and worked with three levels of dereference without too much difficulty, but when you encounter code that's working at five levels… you're definitely wondering WTF is going on, especially as that usually means that at least one of the levels involved is doing something funky with dynamically changing which variables are being updated from deep calls.

    I've had a legitimate case of triple indirection in my codebase (OUT parameter to the address of the next pointer of the previous object in the linked list), but I've since refactored it out.

    If you have 5 levels of indirection, I'd be wondering if some of those levels shouldn't be structs instead.


  • Discourse touched me in a no-no place

    @PleegWat said in OOP is dead:

    I've had a legitimate case of triple indirection in my codebase (OUT parameter to the address of the next pointer of the previous object in the linked list), but I've since refactored it out.

    Yes. The legit 3 level cases I can think of off the top of my head are where there's an OUT parameter for an array of pointers to things.


  • BINNED

    @bobjanova again, that has nothing to do either with pointers (you wrote yourself that references have the same "problem") or with memory-safe languages. If you're writing concurrent code, you need to have a model for how concurrent access happens. You can write a race condition in Java too:

    void addSome()
    {
        accountBalance = accountBalance + 1;
    }
    

    Run this on two different threads and you'll have a race conditions no matter which language you use. Concurrent mutation of a variable needs synchronization.


  • Banned

    @Kamil-Podlesak said in OOP is dead:

    @dkf said in OOP is dead:

    There are far more different ways of doing OO than most developers seem to think. I tend to think that until people have written two or three of them, they don't really know what they're doing on the topic; there are some really complex design decisions that you can't really understand until after you've made the key mistakes involved. 😜

    I think people should at least try CLOS - if nothing else, it shows that even some universally held concepts are not needed. In particular, the assumption that method belongs to the class. Whenever I ask (in these discussions about OOP) why should be the method polymorphism limited to single class hierarchy, people just stare at me.

    Are you saying that there's no such thing as "belongs to" relationship between classes and methods, or that it's a many-to-many relationship?



  • @error said in OOP is dead:

    x = ++x

    code review:
    <prejudice level="HR has something to say to me">
    DENIED!
    </prejudice>



  • @dkf said in OOP is dead:

    @PleegWat said in OOP is dead:

    I've had a legitimate case of triple indirection in my codebase (OUT parameter to the address of the next pointer of the previous object in the linked list), but I've since refactored it out.

    Yes. The legit 3 level cases I can think of off the top of my head are where there's an OUT parameter for an array of pointers to things.

    That might be touching on one of the causes of pointer fear. Out parameters, arrays, and plain pointers are all pointers: each * in ***thing has a different meaning; but at the code level they all look the same, and there's no difference in semantics. Pointers get looked on as making things inherently more obscure and therefore something to be avoided if you want to understand what's going on. Obviously this is why things like typedef exist, but the perception persists.



  • @Watson said in OOP is dead:

    Out parameters, arrays, and plain pointers are all pointers: each * in ***thing has a different meaning; but at the code level they all look the same, and there's no difference in semantics. Pointers get looked on as making things inherently more obscure and therefore something to be avoided if you want to understand what's going on. Obviously this is why things like typedef exist, but the perception persists.

    That's why the C++ core guidelines discourage the use of raw pointers and encourage the use of std::unique_ptr, std::string_view, gsl::span, gsl::owner etc. instead. If you write a modern code base from scratch, you try to avoid "raw" pointers and instead wrap them in types that give them semantic meaning.

    Now the committee just has to get its head out of its ass and finally standardize std::optional for references, because that's one of the last use cases for raw pointer arguments.


  • BINNED

    @dfdub said in OOP is dead:

    std::optional for references

    Put in astd::reference_wrapper, maybe?
    Not that I'd like using it.


  • Java Dev

    @Watson

    My codebase uses way more linked lists than is healthy, so I have this pattern a lot:

    Foo * foo_list, ** pfoo;
    
    for( pfoo = &foo_list ; *pfoo ; pfoo = &(*pfoo)->next )
    

    I've never liked typedef foo* fooptr though, and neither did the previous steward of this codebase.



  • @topspin said in OOP is dead:

    Put in astd::reference_wrapper, maybe?
    Not that I'd like using it.

    Possible, but really not ideal, especially not in generic code. Instead, I still use boost::optional, because we can't get rid of Boost in the foreseeable future anyway (as much as I'd love to). And its implementation is exactly what I and an estimated 80% of the C++ community want from optional.



  • @PleegWat said in OOP is dead:

    linked lists

    I can hear your L1 Cache screaming from here.



  • @dfdub I bet there's something similar in the old desktop publishing program on which I worked. Its engine gave ownership of most objects to an ObjectManager. Said objects had an ID that was unique and fixed as long as the document was open. If you needed to store a reference to the object for any length of time (giving a reference to the object to a user, for example) you used its ID. Later on when you needed a pointer to an object but only had its ID, you'd ask the ObjectManager for it and then dynamic_cast it back to whatever was needed. There may have been less occurrences of the text due to utility functions:

    public static CRectangleObject* GetRectangleFromID(IDType id) {
      return dynamic_cast<CRectangleObject>(GetObjectManager().GetObjectFromID(id));
    }
    

    ...but the actual number of calls would be up there.

    As for the other casts: I was learning C++ at the time so I was big into the new-style casts and const-correctness. My code would use static_ or reinterpret_cast where the other devs would just use (C-style casts) and my (...) const functions would often cast away const so they could call a library function. (I stopped making const functions early on except for simple utility functions on classes I was creating.)

    The most annoying part of working with C++, the STL, and such back then (as a Windows programmer) was Microsoft's STL implementation and Visual C++ 6's complete lack of help in trying to interpret a compiler error that resulted from STL classes. Sadly I don't have Visi 6 installed on a VM or I'd try to come up with a good one for everyone here to guess the problem. Here's a tiny approximation:

    error C2342: something's wrong in std::vector<CString,std::allocator<_Ty>> &std::map<std::wstring,std::vector<_Ty,std::allocator<_Ty>>,std::less<_Kty>,std::allocator<std::pair<const _Kty,std::vector<_Ty,std::allocator<_Ty>>>>>::operator [](const std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t>> &)' and maybe 'std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t>>' too.


  • BINNED

    @dfdub said in OOP is dead:

    @PleegWat said in OOP is dead:

    linked lists

    I can hear your L1 Cache screaming from here.

    Why, it never gets hit. 🍹

    Filed under: memory violence


  • Discourse touched me in a no-no place

    @PleegWat said in OOP is dead:

    I've never liked typedef foo* fooptr though, and neither did the previous steward of this codebase.

    I've seen people become intensely confused from that sort of typedef. You're wise to not like it. The only time I'll willingly use such things in C is where the foo is an incomplete type and I want to have a token that client code can't dereference (akin to a private class in some other languages that one still has to pass a reference around to).


  • Discourse touched me in a no-no place

    @topspin I've seen good uses of linked lists. They work very well when “iterate over all these things in order” is the main use-case and you can't allocate them all at once; with a bit of basic design their memory overhead is small. But if you're searching the list (and it's not usually extremely short) then you've used the strong thing.


  • Java Dev

    @dkf said in OOP is dead:

    @topspin I've seen good uses of linked lists. They work very well when “iterate over all these things in order” is the main use-case and you can't allocate them all at once; with a bit of basic design their memory overhead is small. But if you're searching the list (and it's not usually extremely short) then you've used the strong thing.

    Most of it isn't in key loops. Though a lot of it is also in metadata where realloc() might be a good idea if it ever grows to be a significant bottleneck (which it won't). There's a few places in more recent code I wrote myself where I just allocate 1k items, since that's the DB limit on number of columns.

    A few years back I turned an ordered doubly linked list (a queue of unprocessed objects) into an unordered one, only to hit it with a hand-written mergesort once every 15 seconds when it processes the accumulated data. That helped a lot for performance in edge cases.



  • @dkf said in OOP is dead:

    They work very well when “iterate over all these things in order” is the main use-case and you can't allocate them all at once; with a bit of basic design their memory overhead is small.

    The main issue is that you're chasing pointers and potentially jumping all over memory. The former hinders prefetching, and the latter makes your cache hitrate plummet. With a resizable array/vector, iterating over things in order becomes much more efficient for these reasons. You can even make it a vector of pointers -- because the CPU can see the following pointers already (they're not hidden behind an indirection), it can prefetch much more aggressively. (There's a talk on one of the C++ conferences about this, I can't remember the name right now, but if there's interest, I'll dig it out.)

    The only place that I've found linked lists to be somewhat useful is when building some datastructures on e.g. a GPU. A single-linked list is somewhat easy to create with a few atomics. But even there, it's more of an exception. Pre-allocating a chunk of memory and inserting stuff with an atomically incremented counter is easier, and makes the following steps easier as well.

    My rule of thumb is that one needs a very specific reason to reach for a linked list, and resizable array/vector is the default. I don't remember when I last used a linked list.


  • Discourse touched me in a no-no place

    @cvi said in OOP is dead:

    The main issue is that you're chasing pointers and potentially jumping all over memory. The former hinders prefetching, and the latter makes your cache hitrate plummet.

    It depends on the rate at which you're using the list of things as a collection. In use cases where the list is kept so you can rarely clean the world up, performance isn't a huge concern and it's useful to have the collection kept around at a cost of only one or two pointers per structure (depending on whether you keep a singly- or doubly-linked list).



  • @dkf said in OOP is dead:

    it's useful to have the collection kept around at a cost of only one or two pointers per structure (depending on whether you keep a singly- or doubly-linked list).

    That's a O(N) storage overhead; the vector has O(1), e.g. two/three pointers per vector instance (not per element). This is especially true if you rarely change it, so you can avoid overallocation.

    With overallocation, the worst case is also O(N), but the constant is at most one (growth rate with a factor 2).

    If you go for a vector of pointers, you get a similar overhead as a singly-linked list, except with the vector, elements can trivially be members of multiple lists (assuming an intrusive linked list, which seems to be what you're describing).


  • Discourse touched me in a no-no place

    @cvi said in OOP is dead:

    That's a O(N) storage overhead; the vector has O(1), e.g. two/three pointers per vector instance (not per element). This is especially true if you rarely change it, so you can avoid overallocation.
    With overallocation, the worst case is also O(N), but the constant is at most one (growth rate with a factor 2).
    If you go for a vector of pointers, you get a similar overhead as a singly-linked list, except with the vector, elements can trivially be members of multiple lists (assuming an intrusive linked list, which seems to be what you're describing).

    It sounds like you've convinced yourself. Carry on there.



  • @Gąska said in OOP is dead:

    @Kamil-Podlesak said in OOP is dead:

    @dkf said in OOP is dead:

    There are far more different ways of doing OO than most developers seem to think. I tend to think that until people have written two or three of them, they don't really know what they're doing on the topic; there are some really complex design decisions that you can't really understand until after you've made the key mistakes involved. 😜

    I think people should at least try CLOS - if nothing else, it shows that even some universally held concepts are not needed. In particular, the assumption that method belongs to the class. Whenever I ask (in these discussions about OOP) why should be the method polymorphism limited to single class hierarchy, people just stare at me.

    Are you saying that there's no such thing as "belongs to" relationship between classes and methods, or that it's a many-to-many relationship?

    The first. Although, from some point of view, the second might be also a valid interpretation...


Log in to reply