Earnestly thinking NULL is a mistake is a symptom



  • @Kian said:

    Is Option a language level construct, or some library level generic class?

    It's both, actually. It's a library type made possible only by the way enums work in the language. Think tagged unions.



  • Ok, I'd need to look into how enums are implemented in Rust then. Because in general, Optional types feel like over-complicating something just so you can kick doing a simple check as far back as possible. If you did the check as soon as the ambiguity showed up, instead of trying to maintain a superposition of states, the code should be much simpler to reason about.

    If Rust had added some specific construct just to enable that, I'd be pretty disappointed in its development.



  • @LB_ said:

    Why are those other fields stored in the first place?

    Well the point of smart properties is that they're not, they're calculated from BirthDate.

    As to why you'd want someone's Zodiac sign, gee I dunno, you're a wizard and are offering your services via a web application. Point is, in most programs there are invariants like "if A is null, B is null, otherwise B is never null" that @Gaska's solution can't express, and it forces you to code around them.


  • Discourse touched me in a no-no place

    @Kian said:

    If you did the check as soon as the ambiguity showed up, instead of trying to maintain a superposition of states, the code should be much simpler to reason about.

    Though that might come at a cost of making other code much harder to reason about. No, I can't think of a case right now, but much of coding is about trading off one sort of complexity for another.


  • Banned

    @Maciejasjmj said:

    What, being awfully pedantic? That only applies on this forum, you know.

    Rust's goal is to be a perfectly safe language without runtime overhead. The only to ways to achieve safety are extensive runtime checks or being awfully pedantic at compile time.

    @Maciejasjmj said:

    ```
    public class Person
    {
    //whole lotta properties
    public DateTime? BirthDate { get; set; }
    public int? Age { get { /too lazy to look it up/ } }
    public ZodiacSign? Sign { get { /yeah, you get the drill/ } }
    //...
    }

    Oh - by properties you meant functions, not fields. Well then - depending on what the actual codebase look like, I'd either not bother because you won't use more than one or two of those at the same time anyway, or implement those functions on DateTime rather than Person - because those functions operate on DateTime and not Person. As I said, refactoring time.
    
    @Maciejasjmj <a href="/t/via-quote/51020/147">said</a>:<blockquote>To do which you need a lot more than null checks. It's called validation logic, it's called unit testing, and if you want your software to be bulletproof the two are necessary anyway.</blockquote>
    Bulletproof in the sense your code will never randomly fail unless you explicitly told it to. Bad business logic is explicitly telling it to fail. Unlike crashing due to null dereference, which in most languages is implicit.
    
    @Kian <a href="/t/via-quote/51020/150">said</a>:<blockquote>Is Option a language level construct, or some library level generic class?</blockquote>
    Simple enum with two variants. Nothing you couldn't do yourself. Here: `enum Option<T> { Some(T), None }` - you just reimplemented the stdlib option type. The only thing left is writing all those convenience methods.


  • @Maciejasjmj said:

    Well the point of smart properties is that they're not, they're calculated from BirthDate.

    As @Gaska says, those shouldn't even be part of Person at all because they only operate on the date.


  • Banned

    @Maciejasjmj said:

    Point is, in most programs there are invariants like "if A is null, B is null, otherwise B is never null"

    Also, most codebases are utter shit.



  • @Gaska said:

    or implement those functions on DateTime rather than Person - because those functions operate on DateTime and not Person

    Yeah, extension methods make more sense here.



  • The only situation where optional makes sense, I feel, is if you have some type you really can't have a "default" state for, another type that may have a member of that "no default" type, and you want to have the optional type inside the container type instead of on the heap.

    And even then, I probably wouldn't want to have an Optional<T> style wrapper. One rule to remember is that "where there's one, there's many", and optional types are pretty wasteful (as implemented where I've seen them).

    @Gaska said:

    imple enum with two variants. Nothing you couldn't do yourself. Here: enum Option<T> { Some(T), None }

    Doing something like a tagged union, which is kind of what std::optional does (and why I don't like it), means adding a tag next to the actual value. Since the value has certain alignment requirements, however, and the tag sits next to it, the containing optional type will have to have the same alignment as the original type.

    For something like a string, that might have a pointer and a length, the alignment is 4 or 8 (in 32 or 64 bit arch). The tag is likely one byte, with alignment one, so the optional type will have to add a padding of 3 or 7 bytes to keep the alignment of the base type. Add a couple more "optional" members to your object, and you may be wasting 9 or 21 bytes in padding.

    I don't know how you would handle it in Rust, but in C++ you could simply have some amount of padding inside the class, enough for each optional member without default values, and a single "optional flags" member where you use one bit for each optional member. Then you ensure at the class level that the optional members can only be accessed if the flag for it is set to on.

    When you set the values you then use placement new to construct them in the assigned passing, and use the flag to know if you need to destruct it when the container goes out of scope.

    It would be nice if there could be language level support, but really, it's such an edge case that it hardly feels worth it unless there's a stronger reason to do it.

    For every other use, you just use a raw pointer for optional values, or if you want to have a "TryFind" method in a map or something, an iterator to the end of the map serves as "the value doesn't exist".



  • You claim it's a rare thing and at the same time you're concerned with the memory usage?



  • @LB_ said:

    You claim it's a rare thing and at the same time you're concerned with the memory usage?

    I claim it's a rare thing, and the only time you need it is in a rare edge case where you care about memory placement. So yes, memory footprint in that rare case is important.



  • @Kian said:

    and the only time you need it is in a rare edge case where you care about memory placement.

    I don't follow, what is this case you're thinking of and why is memory a concern?



  • @Kian said:

    The only situation where optional makes sense, I feel, is if you have some type you really can't have a "default" state for, another type that may have a member of that "no default" type, and you want to have the optional type inside the container type instead of on the heap.

    If you can have the optional type in the heap, a smart pointer does everything you need. The only reason you may want it to be "physically" inside the parent object, is if you care about performance. If you care about performance that much, you can't afford to waste bytes in padding, because the more objects you can pack together the better your performance will be.



  • No, I want to have the type inside the optional wrapper regardless of whether the wrapper is on the heap or the stack.



  • Right, I expressed myself poorly. It doesn't matter where the wrapping type is. The question is, why do you care that it's inside the wrapper, if not for performance? Why can't you use a smart pointer?


  • Banned

    @Kian said:

    The only situation where optional makes sense, I feel, is if you have some type you really can't have a "default" state for

    Almost all types have no sensible default. What should default file path be? Default account balance? Default IPC message? Default number? (Note: 0 is bad default number because you can easily encounter it as a result of calculations on non-defaults. NaN could work, if there was NaN for integer types).

    @Kian said:

    Doing something like a tagged union, which is kind of what std::optional does (and why I don't like it), means adding a tag next to the actual value. Since the value has certain alignment requirements, however, and the tag sits next to it, the containing optional type will have to have the same alignment as the original type.

    Well, actually, Rust has a hard-coded rule called "null pointer optimization" that says when there is an enum with exactly two variants, one of which has at least one field of non-zero type and the other is empty, then the enum is guaranteed to have the same size and alignment as the contents of the first variant. Non-zero type is recursively defined as a pointer (the & kind, not the * kind) or any complex type that has at least one non-zero component.

    In short - if T can't be zero, then Option<T> has the same size as T. This doesn't solve the problem for all T that can be zero, but then it is literally impossible to generically solve this problem without extra storage.

    @Kian said:

    I don't know how you would handle it in Rust, but in C++ you could simply have some amount of padding inside the class, enough for each optional member without default values, and a single "optional flags" member where you use one bit for each optional member. Then you ensure at the class level that the optional members can only be accessed if the flag for it is set to on.

    In Rust, you would probably do the same. Would require a fair amount of unsafe code, though.



  • Sorry for me being brief, I should have elaborated on that. Specifically, I want value semantics - it should seem as if the type isn't even being wrapped at all except for the memory overhead of tracking whether the type is there or not. I do care about performance but I'm not at all concerned about the extra memory usage.

    @Kian said:

    Why can't you use a smart pointer?

    Because a smart pointer has somewhat different semantics, and it's expected that it would never be null.



  • @Gaska said:

    Almost all types have no sensible default.

    Not default in the sense of a value you can pass around, but in the sense of a value you can default construct. Some types can't be constructed at all unless you initialize them. An int can be constructed without being initialized, you just need to not read it until you initialize it properly. "Uninitialized" might be better than "default".

    The containing class can then do the required access control.

    @Gaska said:

    In short - if T can't be zero, then Option<T> has the same size as T. This doesn't solve the problem for all T that can be zero, but then it is literally impossible to generically solve this problem without extra storage.
    Which is one reason I don't like optional types.

    @Gaska said:

    In Rust, you would probably do the same. Would require a fair amount of unsafe code, though.
    Another reason I would stay away from optional members.

    @LB_ said:

    Specifically, I want value semantics - it should seem as if the type isn't even being wrapped at all except for the memory overhead
    That's not how any of the optional types work. You always have to check if the object exists, and if it does, extract it from the optional.

    @LB_ said:

    it's expected that it would never be null.
    Why would a pointer be expected to never be null? The whole point of a pointer is that it may be null. Otherwise, you would just have the thing itself, not a pointer.



  • @Kian said:

    That's not how any of the optional types work. You always have to check if the object exists, and if it does, extract it from the optional.

    C++'s std::optional has value semantics. Yes, you have to check if the object exists before you access it, but you don't need to know if it exists to copy and move the wrapper.

    @Kian said:

    Why would a pointer be expected to never be null?

    I did not say that. I'm implying that a smart pointer should never be null. There's a reason to have an optional wrapper alongside a smart pointer wrapper.



  • @LB_ said:

    I'm implying that a smart pointer should never be null.

    And I'm stating outright that that's wrong. Smart pointers just clean up after themselves. They manage ownership, they don't claim to always be pointing at something.



  • You're trying to lump too much behavior into the same concept, which is why raw pointers are discouraged in C++ in the first place. If we want to have the case of an object which may or may not be there (you said this was rare), we have the optional wrapper class for this case. Otherwise, we have smart pointer wrapper classes. Don't combine uses, separate uses instead. It makes for clearer code.


  • Banned

    @Kian said:

    Not default in the sense of a value you can pass around, but in the sense of a value you can default construct. Some types can't be constructed at all unless you initialize them. An int can be constructed without being initialized, you just need to not read it until you initialize it properly. "Uninitialized" might be better than "default".

    Rust has no constructors, so a maiore ad minus there's no concept of default construction - at least in C++ terms. You can (in unsafe code) pretend that uninitialized memory is fully constructed object, though - and I think that's the closest thing to what you want.

    @Kian said:

    Which is one reason I don't like optional types.

    You don't like them because they often take more storage than the raw type (because they store more information, duh!) and in very very very rare case it's a problem? That's harsh. Anyway, what alternative do you see?

    @Kian said:

    Why would a pointer be expected to never be null?

    Because Rust pointers are defined to never be null.





  • He's microaggressed because his name doesn't fit on a driver's license.



  • I knew a guy named J. People would always ask him to spell it, and systems would frequently reject it.



  • There is a professor at my college with last name "O". Also, my preferred username "LB" is often one character too short for most things (for example this site).


  • Banned

    @LB_ said:

    Also, my preferred username "LB" is often one character too short for most things (for example this site).

    At least you can use all the needed letters.



  • @LB_ said:

    You're trying to lump too much behavior into the same concept, which is why raw pointers are discouraged in C++ in the first place.

    Using raw pointers isn't discouraged. Using raw pointers to manage ownership is. There are plenty of places were raw pointers are preferred, especially if a convention is adhered to (specifically, don't ever use raw pointers to manage ownership or lifetime).

    Ok, let me use some examples to explain why optional types, at least in C++ which is what I have most experience with, are redundant. If I missed any reasonable use case, let me know.

    Let's start with function parameters. You write a function and decide that one of the parameters is optional. Meaning, the function can do useful work even if it doesn't have that parameter. You should then declare that parameter as a raw pointer:

    void Foo(MyType* typePtr) {
      // since typePtr may be null, we only ever use it after checking it's not null:
      if (typePtr != nullptr) {
        auto& object = *typePtr;  // We can safely use object within the if block. 
        // The reference is just a convenience so we have value syntax instead of having to deref all the time. 
        // Add a linter check to ensure you only ever dereference to initialize a reference and you'll
        // basically have the same compile time safety guarantee Rust gives you.
      }
      // Out here, using the pointee is wrong but you can pass the pointer along to functions
      // that take raw pointers of the same or parent types.
    }
    

    Ok, what about function outputs? Say, trying to find a value in a map, for example. Having one function to check and another to recover the value is problematic because searching can be expensive, and adding a search cache overcomplicates the design of the class. Well, just return a raw pointer. This can also work with iterators, if you want access to modify the container, not just read the value, where "value not found" is signaled by an iterator to the end of the container.

    MyType* Map::TryGetValue(Key key);
    ...
    // Then somewhere in your code:
    auto maybeValue = map.TryGetValue(myKey);
    if (maybeValue != nullptr) { // if TryGetValue returns an iterator, check against map.end() instead.
      // same as before:
      auto& object = *maybeValue;
      // use object within the if block, never outside of it.
    }
    // but you can still pass the pointer around if you want.
    

    Ok, that covers passing optional values to functions, and getting optional values back from functions (you can use output parameters instead of return values, but then you have to pass a reference to a pointer, or worse, a pointer to pointer if you want to express that maybe you want to get an optional value back). That should cover 90% of the need for optional values.

    The remaining use is in objects that may or may not contain sub-objects. For most of these, smart pointers get the work done:

    using Type1Unique = std::unique_ptr<Type1>;
    using Type2Shared = std::shared_ptr<Type2>;
    
    class MyContainer {
      Type1Unique optionalType1_;  // We are the only owner of this object, if it exists.
      Type2Shared optionalType2_;  // We are not the only owner of this object, if it exists.
      
      public:
        void SetType1(Type1Unique type1);  // After this function returns, we don't know if we own something.
                                           // We could have been passed a null pointer. That's fine.
        void SetType2(Type2Shared type2);  // Same as above. We can be passed a null shared pointer. We don't care.
        Type1* TryGetType1();  // We return the value of the pointer, which was "optional" when we received it, and this is still optional when people ask us about it. We don't return a smart pointer because we're not giving up ownership, just sharing access to the optional member.
        Type2* TryGetType2();  // ditto. 
    };
    

    The only issue here is that accessing your optional objects requires not only the obvious runtime check to make sure it's there: if (optionalType1_ != nullptr) { auto& object = *optionalType1_; }, but following the pointer to wherever, which hurts locality. Keep in mind that this is a performance consideration. In most of you code, chasing a pointer is hardly an issue. If you have an optional string, then having to make two jumps to get at the text may be annoying, but most of the time it won't matter. But let's say you care.

    If you have a default constructible object and you care about the object being inside your container in the memory representation, then you can just stick the object in there and have a single flag variable to signal the state of your possibly multiple optional objects:

    class MyContainer2 {
      std::string optionalString_;
      int optionalInt_;
    
      enum Options { None = 0, StringObject = 1 >> 1, IntObject = 1 >> 2 } options_;  
      // A single variable to keep track of all our optionals, which makes the object more compact instead of 50% padding
    
      public:
        void SetStringObject(const std::string* stringPtr);  // In case someone wants to pass us a string they maybe don't have. Notice it's a raw pointer, meaning we are going to make a copy if it's not null, we are not going to keep the exact object we were passed.
        void SetIntObject(const int* intPtr); // same as above
        std::string* TryGetStringObject();
        int* TryGetIntObject(); // As before, the "Try" prefix lets users know they may not actually have an object.
    }
    
    /* The implementation of these methods is a bit more tricky than before, but that is the cost of performance. 
       If you don't care about performance, using the smart pointers makes the implementation dead simple. */
    void MyContainer2::SetIntObject(const int* intPtr) {
      if (intPtr != nullptr) {
        auto& object = *intPtr;
        options_ |= IntObject;
        optionalInt = object;
      }
    }
    
    // Do the same for the optionalString_, with the obvious changes where required. Then to get them:
    std::string* MyContainer2::TryGetStringObject() {
        if ((options_ & StringObject) != None) {
          return &optionalString_;
        }
        return nullptr;
    }
    
    // If you feel like resetting either object, you can simply have a method 
    void MyContainer2::Reset(MyContainer2::Options options) {
      options_ &= ~options;
    }
    // This method lets you reset any or all of the contained optionals at once, by doing for example:
    // container.Reset(MyContainer2::StringObject | MyContainer2::IntObject);
    

    Finally, we get to the most annoying, most rare case. You have an optional object that isn't default constructible, but you want it to live inside the object. I don't recall the exact syntax, but basically:

    class MyContainer3 {
        char[sizeof(MyType)] optionalBuffer_; // We actually have to make sure the alignment is correct.
                                              // How you do this depends on implementation so I've skipped it.
        enum Options { None = 0, MyTypeObject = 1 } options_;
    
      public:
        void SetMyType(const MyType* myTypePtr);
        MyType* TryGetMyType();
        ~MyType();  // We actually need this now. We didn't need it in any of the previous examples.
    }
    
    // The setter is basically like before, but look at the last line in the if:
    void MyContainer3::SetMyType(const MyType* myTypePtr) {
      if (myTypePtr!= nullptr) {
        auto& object = *myTypePtr;
        options_ |= MyTypeObject;
        new (optionalBuffer) MyType(object); // we copy construct the object into the buffer we created in the class
      }
    }
    
    // The getter is almost exactly the same as for MyContainer2, but we need an ugly cast.
    MyType* MyContainer2::TryGetMyType() {
        if ((options_ & MyTypeObject) != None) {
          return reinterpret_cast<MyType*>(optionalBuffer_);
        }
        return nullptr;
    }
    
    // Reset has to be modified slightly too:
    void MyContainer3::Reset(MyContainer3::Options options) {
      if ((options & options_ & MyTypeObject) != None) {
        reinterpret_cast<MyType*>(optionalBuffer_)->~MyType();  // Manually call destructor
        options_ &= ~MyTypeObject;
      }
    }
    
    // Since we used placement new, destructing the object properly is our responsibility.
    MyContainer3::~MyContainer3() {
      if ((options_ & MyTypeObject) != None) {
        reinterpret_cast<MyType*>(optionalBuffer_)->~MyType();  // Manually call destructor
      }
    }
    

    The third example is the ugliest, but it's also the one with the most constraints. The optional type may be a bit clearer in that specific example, but it's also more wasteful. It would be a very slim niche where "I need more performance with this specific type, but not that much more" would be a reasonable argument.

    @LB_ said:

    (you said this was rare)
    Just to reiterate, what I said were rare were examples with the constraints of MyContainer2 and MyContainer3. In 90% of the cases, raw pointers cover your needs, and smart pointers fill most of what's left.

    @Gaska said:

    Rust has no constructors
    Right, they gave up exceptions. Pity. Anyway, notice I never refer to default constructed objects as if they were valid objects. I only pass pointers to objects if they have been properly set, otherwise I give null pointers. The default initialization is meant to avoid having to use placement new and call the destructor manually.

    @Gaska said:

    Anyway, what alternative do you see?
    I explored several alternatives in quite some depth. Let me know if I missed any use case.

    @Gaska said:

    Because Rust pointers are defined to never be null.
    Right, and then you use enum { RustPointer(), None }, and since a value of zero is not a valid value the optional or however you define it just encodes 0 as the None type, using no more memory. So you end with a value that can be either a pointer to a valid object, or 0. Which is what C++ pointers already were.


  • Winner of the 2016 Presidential Election

    @Kian said:

    Which is what C++ pointers already were.

    Except that you cannot accidentally use a null pointer and get a segfault. It seem like you're still missing the point.



  • @asdf said:

    Except that you cannot accidentally use a null pointer and get a segfault. It seem like you're still missing the point.

    It enforces proper use of pointers by the compiler, I get that. I was pointing out, they didn't change how pointers behave. The enum syntax is nice, I will have to look into getting some pre-compile checks to get something like that into my IDE. But it's not revolutionary.

    And my overall point wasn't about pointers in Rust specifically, but optional types in general. A pointer is just one possible implementation of one possible kind of optional value. The general solution for optional types has fundamental flaws that hinder it in the exact place where something more elaborate than a pointer is required.



  • "Casimer the Adequately Skilled" gets truncated by quite a few games.


  • Discourse touched me in a no-no place

    @ben_lubar said:

    Casimer the Ad

    It'd be neat if it truncated to 14… :)


  • :belt_onion:

    @Gaska said:

    Java 8 introduces @NonNull annotation. Though personally, I'd prefer something like @Strict annotation at package level rather than having to repeat the same annotation over and over again everywhere in all my files (@Strict reduces it to just once in every file; not ideal, but bearable).

    @NotNull is excellent and solves this problem very well for me. I put those (or @Nullable) on almost all of my methods dealing with objects and they allow me to be lazy - no need to add in the null check because if I get a null you've violated my method's contract and I don't even care. Plus most IDEs will give you a warning if you might pass a null or a @Nullable result in to a @NotNull method parameter. And I don't feel satisfied until every little yellow dot on the IntelliJ scroll-bar-warning thing is snuffed out


  • :belt_onion:

    TIL there's a user named @Nullable
    Hi @Nullable, you've been annotation-paged!


  • Discourse touched me in a no-no place

    @sloosecannon said:

    And I don't feel satisfied until every little yellow dot on the IntelliJ scroll-bar-warning thing is snuffed out

    You can probably upgrade @NonNull violations to errors in your IDE. The only reason I don't do that is that I work with libraries/frameworks that lack the annotations (which makes it really annoying, despite otherwise being great).



  • @Kian said:

    If I missed any reasonable use case, let me know.

    Passing an object which may not exist to a function, and expecting the function to take over ownership. Same with returning.


  • Banned

    @Kian said:

    Let's start with function parameters. You write a function and decide that one of the parameters is optional. Meaning, the function can do useful work even if it doesn't have that parameter. You should then declare that parameter as a raw pointer

    No you shouldn't. You should make two separate functions - one taking N parameters and the other taking N-1 parameters. You might want to make one call the other to reduce the code duplication - but this depends on exact code.

    @Kian said:

    Ok, what about function outputs? Say, trying to find a value in a map, for example. Having one function to check and another to recover the value is problematic because searching can be expensive, and adding a search cache overcomplicates the design of the class. Well, just return a raw pointer. This can also work with iterators, if you want access to modify the container, not just read the value, where "value not found" is signaled by an iterator to the end of the container.

    Or optional. Works the same way, except you get all these nifty compile time checks.

    @Kian said:

    The remaining use is in objects that may or may not contain sub-objects. For most of these, smart pointers get the work done (...). In most of you code, chasing a pointer is hardly an issue.

    ...Let me get this straight. You are worried about a dozen or two bytes of bonus padding within object, but not additional dereference?

    @Kian said:

    If you have a default constructible object and you care about the object being inside your container in the memory representation, then you can just stick the object in there and have a single flag variable to signal the state of your possibly multiple optional objects

    There's nothing that prevents you to do it in Rust if you really, really have to. It's basically the same as std::optional, except for multiple values at once.

    @Kian said:

    Finally, we get to the most annoying, most rare case. You have an optional object that isn't default constructible, but you want it to live inside the object.

    In Rust, this would be identical to the previous case, since in both you'd probably use std::uninitialized() to "initialize" the fields.

    @Kian said:

    The third example is the ugliest

    Not so ugly in Rust 😊

    @Kian said:

    The optional type may be a bit clearer in that specific example, but it's also more wasteful. It would be a very slim niche where "I need more performance with this specific type, but not that much more" would be a reasonable argument.

    You keep ignoring the semantical meaning the optional type carries with it.

    @Kian said:

    Right, they gave up exceptions.

    In Poland, we have this saying that goes like, what does gingerbread have to do with windmill?

    @Kian said:

    Anyway, notice I never refer to default constructed objects as if they were valid objects.

    This is why constructors are bad - because of some technical reasons, you really need them even if they don't make sense, and they end up not really being constructors, so existence of default constructor doesn't make it clear whether default-constructing the object is a valid usage.

    @Kian said:

    I explored several alternatives in quite some depth. Let me know if I missed any use case.

    A generic data structure that holds data in place (ie. not in remote location accessible by pointer), gives you all the value semantics you want and allows you to directly obtain reference to the contained object.

    @Kian said:

    It enforces proper use of pointers by the compiler, I get that. I was pointing out, they didn't change how pointers behave. The enum syntax is nice, I will have to look into getting some pre-compile checks to get something like that into my IDE. But it's not revolutionary.

    Optionals themselves aren't really revolutionary - aside from being invented decades ago, they don't really change the runtime code that much either. What is revolutionary, however, is the mindset of using optional everywhere when applicable, and having the compiler automatically check if you haven't perhaps forgotten a null check in one place or another.


  • Banned

    @sloosecannon said:

    TIL there's a user named @Nullable Hi @Nullable, you've been annotation-paged!

    Apparently, you quoted me here.

    It's a shame @discoursebot doesn't work anymore. I loved that little fellow.



  • @‍Gaska - Days Since Last Discourse Bug: 0


  • Banned

    Oh, it's working. Weird. @discoursebot



  • @Slapout said:

    NULLGO TO is a valueflow control that is not a valuestructured flow control. And that’s a problem And therein lies it's power.

    The thing is, power comes with penalties. Null might be powerful, but because it is, it also is a powerful way to shoot yourself in the foot.@Gaska said:
    As any professional boxer will tell you, with great power comes great headache.

    Yeah, that too.
    @Gaska said:
    Bad business logic is explicitly telling it to fail.

    I don't think that's true. Better to tell it to fail than to use a bad value to generate business logic results. @Maciejasjmj said:

    As to why you'd want someone's Zodiac sign, gee I dunno, you're a wizard and are offering your services via a web application. Point is, in most programs there are invariants like "if A is null, B is null, otherwise B is never null" that @Gaska's solution can't express, and it forces you to code around them.

    Age is very important in medicine, and it's hard to calculate age without a birth date. It's not always a matter of astrological signs. In a similar vein, there are many other properties that are more than just data-holding fields. Think of blood type in medicine, that's kind of something you need to know and it's kind of important it's right.
    @LB_ said:

    You claim it's a rare thing and at the same time you're concerned with the memory usage?

    It is a con. If you don't have better cons, then you bring up memory.
    @xaade said:

    He's microaggressed because his name doesn't fit on a driver's license.

    ...and all the rest of us are microagressed because his name is longer than ours.


  • Banned

    its*


  • Banned

    @CoyneTheDup said:

    I don't think that's true. Better to tell it to fail than to use a bad value to generate business logic results.

    Fail, as in fail to perform the task as specified.

    @CoyneTheDup said:

    ...and all the rest of us are microagressed because his name is longer than ours.

    I'd say it's macroagression :P



  • @Kian said:

    Ok, what about function outputs? Say, trying to find a value in a map, for example. Having one function to check and another to recover the value is problematic because searching can be expensive, and adding a search cache overcomplicates the design of the class. Well, just return a raw pointer. This can also work with iterators, if you want access to modify the container, not just read the value, where "value not found" is signaled by an iterator to the end of the container.

        MyType* Map::TryGetValue(Key key);
        ...
    ```</blockquote>
    
    There's not always a pointer/iterator to return, since the object you're returning may be created within the function you're calling. You could put it on the heap then and return an `unique_ptr`, but that's not always an acceptable solution (because of the extra alloc).
    
    If you want to return the object by value, and there's no cheap default-constructible state that you can use to flag the objects invalidity, you're pretty much stuck with something like an `optional<>`. 
    
    The overhead of the extra flag in the `optional<>` on the stack is at this point probably negligible in most cases, and if your function is inlined, it might disappear completely.
    
    
    As for members, I can see `optional<>` being useful if you do stuff like reflections (either with a pile of ugly macros, with an external tool, or hopefully soon with standard stuff (SG7)). Then it's kinda neat to have the type of a member tell you it's optional.
    
    @Kian <a href="/t/via-quote/51020/178">said</a>:<blockquote>`// We actually have to make sure the alignment is correct.`</blockquote>
    
    Use `std::aligned_storage` (TR1/C++11).

  • Discourse touched me in a no-no place

    @cvi said:

    You could put it on the heap then and return an unique_ptr, but that's not always an acceptable good solution (because of the extra alloc).

    The cost of the heap can be greatly reduced if you know what's going on with threads in your application more thoroughly; most of the actual cost of allocation is in acquiring the global resource that is allocatable memory (unless you have to actually go and ask the OS for another empty page, of course).

    new is just an abstraction. With all abstractions, you sometimes are better off looking at what the abstraction hides. And sometimes not. 😄



  • Sure, new is quite fast ... at times. Sometimes you can make it even faster by changing the allocator (is tcmalloc still a thing?), but that's relatively painful. You can also do stuff like temporary buffers or pools, perhaps with thread locals, but that's also a pain (you need to free that memory in a different way).

    But you're at best going to be on par with a stack allocation (at the very least, both need to increment/decrement some counter and then potentially initialize the newly available memory). And stack allocation is, in terms of performance, rather more robust than the heap.

    Finally, if you're inlining stuff, the optimizer can do a lot more work with stack objects than with heap objects (at least in the C++ world). Smaller "stack" objects don't even need to use any memory, but can completely stay in registers. Using optional<> doesn't change this; going via the heap does.


  • :belt_onion:

    @loose said:

    But magic to work with when you do understand. I am not going to say what I have done with pointers because it would be a very revealing statement @obeselymorbid

    Huh?



  • Most tools allow external annotations for that.



  • @Gaska said:

    I have seen null-or-false checks on fucking booleans.

    Don't forget FileNotFound checks 😉


  • Discourse touched me in a no-no place

    @cvi said:

    And stack allocation is, in terms of performance, rather more robust than the heap.

    But requires that you can bind the lifespan of the value to an owning stack context.


Log in to reply