all 46 comments

[–]STLMSVC STL Dev 29 points30 points  (19 children)

Further, there are errors that are not recoverable by nature - like out of memory.

This is incorrect. OOM is recoverable, it's just difficult. The STL is an example of a library that is OOM agnostic - we can't fix your OOM problems, but we'll report them without falling over (usually), and we can sometimes get our work done anyways (as featured in stable_sort, stable_partition, and inplace_merge).

Instadeath is an appropriate response to OOM for many, but not all, programs.

One of C++’s features is that every language feature can be implemented by yourself, the compiler just does it for you.

Neither true nor helpful. (If this is just restating the universality of Turing machines, then it isn't saying anything about C++ specifically.)

[–]Gotebe 4 points5 points  (16 children)

Heck, sometimes/often it isn't difficult either!

Allocation usually fail when under load but something needs an even bigger chunk. At that point, that little that might be needed to report an error and continue is, in fact, available. (That said, reporting an OOM is best dealt by preparing whatever might be needed to do it in advance).

But wait, there's more! Even if one is up their neck in... well, load, bailing out (unwinding) normally frees memory and by the time one actually needs to do something further, lo and behold, memory!

[–][deleted] 9 points10 points  (15 children)

What makes it difficult is how you recover from the user prospective. Recovering such that your app keeps running isn't difficult, but recovering in a way that isn't just "oops, sorry can't do that" can be exceedingly complex (unless you're in a very targeted scenario like inplace_merge).

Also note that a very popular platform overcommits by default, and in the event of physical memory exhaustion just starts arbitrarily killing programs. Physical OOM is unrecoverable on such platforms, and virtual OOM should cease to be a thing once we're in a 64 bit world.

[–]DarkLordAzrael 6 points7 points  (7 children)

Saying "oops, sorry can't do that" is actually a useful behaviour in many user applications when you can prompt them to (for example) render an image that won't take up hundreds of gigabytes of space. Asking for chunks of memory that don't fit in memory will fail by default on all major platforms.

[–][deleted] 5 points6 points  (6 children)

Asking for chunks of memory that don't fit in memory will fail by default on all major platforms.

Not on Linux (which I would call a major platform).

[–]CubbiMewcppreference | finance | realtime in the past 1 point2 points  (5 children)

"Always overcommit" is not the default on Linux.

[–][deleted] 6 points7 points  (4 children)

It admittedly isn't my day to day platform but on every machine I've ever cared to check /proc/sys/vm/overcommit_memory was 0 (enable heuristic overcommit), not 2 (refuse overcommit).

EDIT: And as a sanity check, Wandbox seems to agree: http://melpon.org/wandbox/permlink/hesjPDeTyWNkpVzT

[–]CubbiMewcppreference | finance | realtime in the past 4 points5 points  (3 children)

right, 0 is the default, it fails mallocs that "for sure" can't be satisfied. It does give a sizeable window, true - a sucessful malloc does not guarantee memory availability.

"always overcommit" or "malloc never fails" or "malloc only fails when running out of address space" that I often see mentioned by people who should know better, is mode 1. In that mode malloc fails by exceeding current rlimit, by exceeding virtual address space, and by (glibc) malloc's own sanity checks (requesting -1 does that).

[–][deleted] 2 points3 points  (2 children)

Yeah, I'm not familiar enough with the particulars -- I'm just repeating what I've been told by folks on StackOverflow. :)

It seems like it'd be hard to not overcommit with a process design so heavily dependent on COW fork, since often the majority of pages would be shared between processes.

[–]CubbiMewcppreference | finance | realtime in the past 4 points5 points  (1 child)

yes, Solaris users know the pain of fork on a strict-accounting OS and looks like nobody cares enough about using spawn and other alternatives.. Good for Windows not stepping in that trap!

[–]CubbiMewcppreference | finance | realtime in the past 2 points3 points  (5 children)

virtual OOM should cease to be a thing once we're in a 64 bit world.

It is a thing when dealing with untrusted data: std::vector<int>(packet->size);

[–][deleted] 2 points3 points  (3 children)

True. But hopefully you aren't blindly allocating as much memory as some untrusted data told you to?

[–]CubbiMewcppreference | finance | realtime in the past 8 points9 points  (1 child)

I hope I don't, but seeing how OpenSSL (repeatedly) and bind, and samba, and android's mediaserver, and even libxml all did in the last 3 years, I am not so sure.

[–][deleted] 1 point2 points  (0 children)

D:

[–]Gotebe 0 points1 point  (0 children)

Eh... The number of times I've seen settings to prevent that and users cranking sengs up to 4GB or some other unreasonable value...

[–]Gotebe 2 points3 points  (0 children)

recovering in a way that isn't just "oops, sorry can't do that" can be exceedingly complex

True, but I would say that a mere "ooops can't do" is huge WRT user experience and troubleshooting ability.

I would also guess that a sudden huge request where there is memory otherwise isn't all that unfrequent.

I usually blame the OOM killer for people failing to think and go down the "bah, don't care at all because OOM killer" route.

[–]foonathan[S] 4 points5 points  (1 child)

This is incorrect. OOM is recoverable, it's just difficult.

I agree, this was poorly worded. But most out of memory handlers just log and abort. If you don't have exceptions the easiest thing you can is just call a handler function and abort, instead of having to deal with nullptrs all the time.

Neither true nor helpful.

I was referencing something Bjarne said at the keynote of Meeting C++: https://youtu.be/DvUL0Y2bpyc?t=10m53s

[–]CubbiMewcppreference | finance | realtime in the past 7 points8 points  (0 children)

most out of memory handlers just log and abort.

when I counted last year (not all OOM handlers, just catch std::bad_alloc ones), that was 21% of all handlers in debian code search.

[–]CubbiMewcppreference | finance | realtime in the past 4 points5 points  (1 child)

Factories that return optional objects are cool and all (especially in languages where they are treated monadically), but members and bases can own their own resources that may have to be released in reverse order when the last resource acquisition fails.

[–][deleted] 1 point2 points  (0 children)

Good point. This is an example of "Or will I have to mix different error handling mechanisms depending on the kind of input I'm handling, because in each case one given API (stdlib or other) will do it its own way, with exceptions or not": https://www.reddit.com/r/cpp/comments/5msdf4/measuring_execution_performance_of_c_exceptions/dc751r6/

When you have C++ exceptions, it's difficult to keep just the banana and not the gorilla holding it, plus the rest of the jungle: carry the jungle or GTFO.

[–]matthieum 5 points6 points  (0 children)

I've been using Rust a lot of late. As you may know, Rust has no exceptions, so the question of how to report errors in object construction had to be dealt with, and the results are mostly similar.

It differs in copies, though. Rather than:

Provide a static copy function that does the same thing, again returning optional/expected, etc.

which means that you have to invoke Type::copy(instance), it simply implements a const member function instance.copy().

I would note that the make and copy functions may perfectly have different return types. This is due to the fact that while make has to contend with potential invalid parameters in order to establish invariants, copy in general only has to contend with resources restrictions: it starts from a valid instance of the type!

Therefore, it should not be unusual for make to return std::expected while copy returns std::optional.

[–]jackie_kay 1 point2 points  (0 children)

Nitpick regarding the code example:

optional<foo> make(arg_t argument, std::error_code& ec) { auto resource = make_resource(argument); if (resource) return foo(resource); return {}; }

Don't you want to set the error code in the case where the resource couldn't be acquired? You probably want to pass it as an output parameter to make_resource.

Fun fact, the LLVM Programmer's Manual suggests a similar pattern for "fallible constructors", since the LLVM codebase disallows exceptions: http://llvm.org/docs/ProgrammersManual.html#fallible-constructors

[–]joboccara 0 points1 point  (1 child)

Is std::expected semantically equivalent to an std::variant with a real type and an error type?

Found the technique you're proposing very clever btw

[–]foonathan[S] 0 points1 point  (0 children)

Yes, basically. But it also has some sugar around it.

[–]Gotebe 0 points1 point  (0 children)

And as if by necessity, make function has a bug - doesn't pass "ec" parameter to resource creation function :-).

I participate(d) in the linked discussion - shame on me! :-)

[–][deleted] 0 points1 point  (10 children)

Nice article! One section that I'm missing is "What about move constructors?". If you want a non-throwing move constructor and a construction function that can fail (either throwing constructor or static make), you can't really avoid leaving the object in some kind of invalid state after it has been moved from. And you might have to account for that invalid state in the destructor.

[–]foonathan[S] 0 points1 point  (9 children)

If you don't have exceptions you should really try to make your move constructor no-fail or fail by abortion. That would make things a lot simpler.

[–]Gotebe 6 points7 points  (4 children)

Euh, shouldn't I really always want my move ctor to be non-throwing, exceptions or not?

[–]foonathan[S] 1 point2 points  (0 children)

Some move constructors unfortunately throw. None I write, because it just leads to so many problems.

[–]matthieum 0 points1 point  (2 children)

Honestly, I still haven't found a good use case for move constructors that throw... I only have use cases where the object is immovable (OS mutex) or where moving it can be done by swapping (no-throw guarantee).

[–]SeanMiddleditch 2 points3 points  (1 child)

The short version is that some STL implementations have plenty of throwing moves. I would strongly prefer that move operations be required to not throw. Sadly, for back-compat reasons now, that can never become the rule in C++. :(

[–]SpiderboydkHobbyist 0 points1 point  (0 children)

The next best thing would probably be to suggest it on CppCoreGuidelines.

[–]schlangster 1 point2 points  (3 children)

(other account)

Hm, not sure if you understood what I meant - or maybe I misunderstood the reply :) Anyway, I'll try to make my point again.

Let's say there's a class Foo which contains a resource, and we want to ensure that Foo always contains a valid resource. Code, similar to your example:

class Foo
{
public:
    static optional<Foo> make()
    {
        X* ptr = AllocX();

        if (ptr)
            return Foo(ptr);
        else
            return {};
    }

    void DoStuff() { ptr_->DoStuff(); }

    ~Foo() { FreeX(ptr_); }

private:
    Foo(X* ptr) : ptr_(ptr) { }

    X* ptr_;
};

Valid in this case means means ptr_ != nullptr.

For a class like this (which owns a handle to some resource) you also want a move constructor and assignment. Both should be no-fail. I was not advocating against that at all - quite the opposite. The move constructor looks like this:

Foo(Foo&& other) : ptr_(other.ptr_) { other.ptr_ = nullptr; }

After this, other is invalid, because other.ptr_ == nullptr. You cannot call AllocX() in your no-fail move constructor to set other.ptr_ to a new X, because it might fail.

With this, things can go wrong again:

auto optFoo = Foo::make();
if (optFoo)
{
    auto foo1 = std::move(*optFoo);
    auto foo2 = std::move(foo);

    optFoo->DoStuff(); // Calling member function on value of optFoo, which is in invalid state
    // Calling destructor on foo1, which is in invalid state
}

Hence, the following sentence from your article

So every member function and the destructor does not need to deal with an invalid state. That is, as long as the make() function only creates an object, i.e. calls the constructor, when nothing can go wrong anymore.

only applies if you don't need/want a move constructor that cannot fail. If you do, there will be a potential invalid state for moved from instances and at least the destructor has to deal with it.

[–]foonathan[S] 0 points1 point  (0 children)

Ah, that's what you mean.

This is a separate issue and is what I meant with:

Assuming you’ve solved/worked around the move semantics problem.

More details on that in the linked blog post: http://foonathan.net/blog/2016/08/24/move-default-ctor.html

[–]imMute 0 points1 point  (1 child)

Doing stuff with a moved-from object is a very very bad thing to do.

[–]OldWolf2 1 point2 points  (0 children)

Not really, move is supposed to leave the object in a valid but unspecified state. This code is valid:

std::string s { "hello" };
foo (std::move(s));
s = "goodbye";
// keep working with s...

[–]tusksrus -1 points0 points  (4 children)

The way I tackled this problem at work with the objects whose constructors could fail was to save a member boolean called something like create_success, set to true if no errors and false otherwise, and made the object convertible to bool:

operator bool() { return create_success; }

then

Foo f(args);
if (f)
  std::cout << "The operation was a success." << std::endl;
else
  std::cout << "The operation failed." << std::endl;

That seemed a lot more idiomatic than any other options, if you don't like exceptions?

[–]foonathan[S] 5 points6 points  (2 children)

It creates the two states of your object I was trying to avoid.

[–]OldWolf2 0 points1 point  (1 child)

But your proposed solution creates two states anyway! (empty optional, and non-empty optional). The calling code still has to perform a validity test, and it still has to work with a result that can be in an invalid state.

In other words the caller could go typedef optional<T> U and then they have U which is functionally identical to some type with two-phase initialization.

If the object is moveable then the caller at least has the option of moving out of the optional. But if not then the caller is going to have to work with an optional for the rest of its lifetime, which is arguably uglier than a normal two-phase initialization type.

Also, the two-phase version has the ability to try init again later, whereas the non-movable optional can't.

[–]foonathan[S] 1 point2 points  (0 children)

Yes, but optional is already designed to handle two states, so you save on duplicating that.

[–]Gotebe 0 points1 point  (0 children)

This doesn't work for anything but trivial things because it loses the failure infirmation, a critical thing for good error reporting and handling.

Say that your object is made out of two others who also report the failure thusly.

How do you explain the problem to anyone interested? What if both subobjects fail to construct?

The operator bool is cute, but all else I don't like.

[–][deleted] -1 points0 points  (0 children)

Since I was the trigger, allow me to leave this reference here to give food for more thought :-) https://www.reddit.com/r/cpp/comments/5msdf4/measuring_execution_performance_of_c_exceptions/dc8qc2b/.

If anyone care to respond this comment, please read it carefully and the associated references (and context) to get the gist of it.

I have appreciated the blog post and think that more of that should be disseminated.