6

Some time ago I was looking for a way to invoke std::async without the need of storing std::future, thus not blocking the execution at the end of the scope. I found this answer which uses a captured std::shared_ptr for an std::future, therefore allowing to make a nonblocking call to std::async.

Another way of deferring a destructor invocation is to prevent it from to be called at all. This can be achieved with in-place construction with operator new.

Consider this version that uses a static thread local storage for an in-place constructed std::future<void>:

template <class F>
void call_async(F&& fun) {
    thread_local uint8_t buf[sizeof(std::future<void>)] = {0};
    auto fut = new(buf) std::future<void>();
    *fut = std::async(std::launch::async, [fun]() {
        fun();
    });
}

This version will not produce any heap-allocation related overhead, but it seems very illegal, though I am not sure why in particular.

I am aware that it is UB to use an object before it has been constructed, which is not the case. I am not sure why not calling delete in this case would resolve in UB (for heap allocation it is not UB).

Possible problems that I see:

  • calling a constructor on one object multiple times
  • race condition when modifying the state (inner std::promise I suppose)

https://ideone.com/C44cfe

UPDATE

Constructing an object in the static storage directly (as has mentioned IlCapitano in the comments) will block each time a move assignment is called (shared state will be destroyed blocking the thread which has removed last reference to it).

Not calling a destructor will case a leak because of not released references to the shared state.

Sergey Kolesnik
  • 3,009
  • 1
  • 8
  • 28
  • 1
    So, basically, `std::future` is not really relevant to the question. The question is is it valid to `void func() { char buf[sizeof(T)]; new (buf) T(); }` construct an object with placement new on stack and not call ever destructor at all? Because as for async: https://stackoverflow.com/questions/52130091/c17-stdasync-non-blocking-execution/52130280 – KamilCuk Apr 01 '21 at 10:10
  • No. The question mentions several possible problems, and I would like to know if there's something I missed. Regarding destructor: I've mentioned it already: for heap allocated objects destructor will also not be called without delete. – Sergey Kolesnik Apr 01 '21 at 10:15
  • @KamilCuk I've forgot to address you, in the comment above. Your link is about detaching a constructed thread. Thread construction is way too slower than calling std::async – Sergey Kolesnik Apr 01 '21 at 10:18
  • Are you intending to call this only once per thread? – Caleth Apr 01 '21 at 10:26
  • @Caleth no. At first it was `static`, but then I thought of making it `thread local` to get rid of race condition for in-place construction in shared memory. One thread might call it as many times as it wants: in `main` starting 50 asynchronous calls. – Sergey Kolesnik Apr 01 '21 at 10:32
  • 1
    You could probably use `thread_local std::future fut` to avoid not calling the desturctor. – IlCapitano Apr 01 '21 at 10:33
  • @IlCapitano this looks better. You mean "to avoid calling the destructor" – Sergey Kolesnik Apr 01 '21 at 10:34
  • @IlCapitano the only question is: does move assignment force to wait for previous async call completion? cppreference doesn't mention it. If it did, you would possibly get blocking on next asynchronous call. – Sergey Kolesnik Apr 01 '21 at 10:43
  • 2
    You could just use `new` to allocate the `future` instance. Obviously, this leaks, but if you think about it, you will find that your version is not guaranteed to not leak. `future` could allocate additional resources which your versions leaks. Actually, it is meant to hold a reference to a resource! Also, other code in the implementation could reference the `future` instance so overwriting it would not just leak but cause errors like invalid/dangling references. – Ulrich Eckhardt Apr 01 '21 at 10:49
  • @UlrichEckhardt that is a very solid point. I suggest you add it as an answer – Sergey Kolesnik Apr 01 '21 at 10:51

2 Answers2

3

It's undefined behaviour to end the lifetime of a non-trivial object without calling it's destructor, which happens as soon as there is a second call_async invocation.

"heap-allocation related overhead" is a misnomer if the only alternative is undefined behaviour. The future returned by async has to live somewhere.

The updated code has defined behaviour: it waits for the previous invocation to be done before launching the next one.

Caleth
  • 52,200
  • 2
  • 44
  • 75
  • I've updated the question. Is the new implementation ok? – Sergey Kolesnik Apr 01 '21 at 11:10
  • @SergeyKolesnik OK in the sense that it has defined behaviour. It doesn't do what you want, there is no way of having *multiple* `async` calls in-flight without having space for *each* of the resulting `future`s – Caleth Apr 01 '21 at 11:15
  • I was wondering if a blocking occurred on move assignment, since on cppreference it is not mentioned as in destructor: `Releases any shared state and move-assigns the contents of other to *this. After the assignment, other.valid() == false and this->valid() will yield the same value as other.valid() before the assignment.` – Sergey Kolesnik Apr 01 '21 at 11:18
  • 1
    The shared state in a future that was sourced from `async` blocks in it's destructor – Caleth Apr 01 '21 at 11:19
  • _It's undefined behaviour to end the lifetime of a non-trivial object without calling it's destructor_ Any wording supporting this? – Language Lawyer Apr 01 '21 at 11:29
  • @LanguageLawyer "A program is not required to call the destructor of an object to end its lifetime if the object is trivially-destructible or if the program does not rely on the side effects of the destructor." [cppreference](https://en.cppreference.com/w/cpp/language/lifetime) And placement-newing over an existing object ends it's lifetime – Caleth Apr 01 '21 at 11:40
  • 1
    IIUC there is [tentative agreement](https://github.com/cplusplus/draft/pull/2342#issuecomment-436158090) that this wording should be [removed](https://github.com/cplusplus/draft/pull/2342/files). – Language Lawyer Apr 01 '21 at 12:02
  • 1
    Ok, that would mean that it is defined behaviour if it were user code. But the standard doesn't specify how the completion of the future is signaled, so it's implementation-defined? I'd still not want to rely on that. – Caleth Apr 01 '21 at 12:14
1

Calling std::async and ignoring the result sounds like "fire and forget". The simplest way to do that is to not use std::async, but to create a detached thread:

std::thread thr(func, data...);
thr.detach();
Pete Becker
  • 74,985
  • 8
  • 76
  • 165
  • it is the easiest, but very expensive. `std::async` uses thread pool which is way faster – Sergey Kolesnik Apr 01 '21 at 14:21
  • @SergeyKolesnik -- `std::async` is not required to use a thread pool, and `std::thread` is allowed to use one.. If you have compiler-specific reasons for the approach you're talking about you should spell them out. – Pete Becker Apr 01 '21 at 14:38