8

In C++17 and above, guaranteed copy elision means that it's possible to return non-moveable objects frun a chain of functions, all the way to the ultimate caller:

struct NonMoveable {
  NonMoveable() = default;
  NonMoveable(NonMoveable&&) = delete;
};

NonMoveable Foo() { return NonMoveable(); }
NonMoveable Bar() { return Foo(); }
NonMoveable Baz() { return Bar(); }

NonMoveable non_moveable = Baz();

Is there some trick to disable guaranteed copy elision for a particular type, so that it's not possible to write functions like Bar and Baz above that pass through a NonMoveable object obtained from another function? (I'm agnostic as to whether Foo should be disallowed or not.)


I know this is a weird request. If you're interested in what I'm trying to do: I'm working on a coroutines library, where the convention is that if you return a Task from a function then all of the reference parameters for that function need to remain valid until the task is ready, i.e. until a co_await expression for the task evaluates. This all totally works out fine if a coroutine is called from another coroutine, since I've arranged for the Task type to be non-moveable and accepted by value: you can't do anything with it except immediately co_await it, and any temporaries you provide in the call to the child coroutine will live until the co_await expression evaluates.

Except you can do one more thing with Task: write functions like Bar and Baz above that do return Foo() instead of co_return co_await Foo(). If there is an argument to Foo involved it might be a temporary, in which case it's safe to co_return co_await Foo(...) but not return Foo(...).

This can get surprisingly subtle. For example a std::function<Task(SomeObj)> internally contains a return statement and will happily bind to a lambda that accepts const SomeObj&. When it's called it will provide the lambda a reference to its by-value parameter, which is destroyed when the lambda suspends.

I'm looking for a more elegant way to prevent this problem, but in the meantime I'd like to make the problematic form just not compile (which also helps identify the set of problems in user code). I expect this is not possible, but perhaps there is some trick I haven't thought of.

jacobsa
  • 5,719
  • 1
  • 28
  • 60
  • 1
    You could `std::move` it. But that disables the copy elision for movable objects too. You can use a concept / enable_if. – Goswin von Brederlow Jul 05 '22 at 10:16
  • I’m not trying to disable it in a particular function; I want to disable it in _all_ functions that return a particular type. It shouldn’t be possible to return the result of another function that returns this type. – jacobsa Jul 05 '22 at 10:37
  • @jacobsa: "*you can't do anything with it except immediately co_await it, and any temporaries you provide in the call to the child coroutine will live until the co_await expression evaluates.*" You can store it in a variable and use it later. Any temporaries passed to the function will end their lifetimes once the variable is initialized, and thus are no longer valid. What you want will not fix the problem you're trying to fix. – Nicol Bolas Jul 05 '22 at 13:45
  • Can you perhaps show some code that fails? Maybe there is a way to make it not fail, or not compile, that doesn't involve weird and impossible stuff like disabling copy elision? IOW what's the X problem? – n. m. could be an AI Jul 05 '22 at 14:21
  • @NicolBolas: make sure to see the [link](https://stackoverflow.com/questions/4850674/is-it-possible-to-restrict-class-instances-to-be-used-only-as-temporaries/72169170#72169170) in the text you quoted. If you own the API ecosystem around the type you can make it impossible to do anything useful with the variable, which is nearly as good as making it impossible to initialize that way, since it makes it hard to have an accidental use of this kind. (If I'm wrong in that link please let me know there.) – jacobsa Jul 06 '22 at 01:20
  • @n.1.8e9-where's-my-sharem.: I shared both the reason I want this and a toy example that should be made impossible in the original question. – jacobsa Jul 06 '22 at 01:21

1 Answers1

1

You cannot disable guaranteed elision. It is a property of the concept of a prvalue in C++17 and above. It's not something any type can affect.

Furthermore, even if you could, this would not get you the effect you want. A prvalue can still be used to initialize a named variable directly. Once that happens, any temporaries used in the constructor call (for const& or && parameters) will have their lifetimes ended. If there were any reference parameters given to the constructor, they may no longer exist.

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • Regarding the first paragraph: yes, this is what I fear. I can't rule out the possibility that there is some trick I haven't thought of though. – jacobsa Jul 06 '22 at 01:17
  • Regarding the second paragraph: it's always true that you could initialize a `Task` variable directly, but it's possible to [restrict](https://stackoverflow.com/questions/4850674/is-it-possible-to-restrict-class-instances-to-be-used-only-as-temporaries/72169170#72169170) the APIs that accept a `Task` so that you can't actually do anything with it afterward, which is almost as good because it makes it hard to do by accident. This is an example of the sort of trick I'm hoping to find here. – jacobsa Jul 06 '22 at 01:18
  • @jacobsa: "*it's possible to restrict the APIs that accept a Task so that you can't actually do anything with it afterward*" And thereby make the API *useless*. If your API *forces* me to use a coroutine, to explicitly make my code halt its execution until *your code* continues it for me, your API is dysfunctional. Pretty much every coroutine API has *at least* a way for you to say "I want to wait, synchronously, for the value, right now". If your API can't do that, then it's not a good API. – Nicol Bolas Jul 06 '22 at 03:11
  • I think you're imagining something different than I'm describing. The point is that the relevant promise method's signature is `void await_transform(Task)`, accepting by value so that you must immediately `co_await` and therefore we can count on all reference parameters remaining valid. That's exactly the API you describe. There is a separate API for manipulating a running coroutine as a value and joining later without the need to require reference parameters to remain valid, but that is necessarily less efficient. – jacobsa Jul 07 '22 at 04:45
  • Oh I guess you're talking about the bridge from synchronous code. But that's also totally unrelated to whether it accepts by value: `void WaitForTask(Task)` also accomplishes what we want (temporaries used in the call to the `Task`-producing coroutine won't be destroyed until the task finishes). – jacobsa Jul 07 '22 at 04:47
  • @jacobsa: But I can't give that task to someone else. I can't store that task in another location which will deal with it in its own time. I must, *immediately upon generation*, deal with it however it will be dealt with. That's insane for any construct calling itself a "task", all to "preserve reference parameters." Here's a better idea: don't allow them. Do what `std::thread` does and *copy arguments* by default. If it's important to ensure that arguments are still alive, then *make them alive*; don't break your API by forcing people into narrow use cases. – Nicol Bolas Jul 07 '22 at 05:06
  • Yes, that’s the other API I referred to, which works like `std::thread`, copies arguments, and is moveable and joinable later. In fact I agree and it’s called `Task` while the non moveable one is called something else that’s not germane to this question. Distinguishing the two is useful because the common case is not needing to do anything but immediately await the result of a call, so you can be significantly more efficient if you’re not forced to copy arguments. – jacobsa Jul 07 '22 at 11:20