4 ms·
I completely agree with most of the post. In C++ I use the static factory method "trick" mentioned in the post, when constructing subobjects may throw or result
by khold_stare 7y ago
I completely agree with most of the post. In C++ I use the static factory method "trick" mentioned in the post, when constructing subobjects may throw or result in some kind of error. Throwing from the middle of a constructor in C++ is a quagmire, so it's best not to do it. Bonus, you can mark the constructor and factory functions noexcept.
The part I disagree with relates to "relying on the optimizer for placement". Even in C++ using the above factory pattern, you are returning the constructed object from a function - and there is no problem if it is ultimately part of some larger object. The C++ standard specifies copy-elision very precisely so you don't have to hope the optimizer does it - it is required to. To demonstrate you can do stuff like this even if you object contains non-moveable members, like std::mutex
class Foo
{
private:
std::mutex mutex_;
SomeComplexSubObject sub_;
Foo(SomeComplexSubObject sub) noexcept
: sub_{std::move(sub)}
{ }
public:
static std::optional<Foo> make(SomeParams params) noexcept
{
try {
return Foo{SomeComplexSubObject{params}};
}
catch (std::exception const& e) {
return std::nullopt;
}
}
};
I think Rust can also specify something like this (ie. "copy-elision") as part of its unwritten spec. Anyway, great article! :)
- stinos 7y agoThrowing from the middle of a constructor in C++ is a quagmire, so it's best not to do it https://isocpp.org/wiki/faq/exceptions#ctors-can-throw https://isocpp.org/wiki/faq/exceptions#ctors-can-throw for one disagrees, but it might not cover what you mean exactly. So: also in modern C++ (i.e. using RAII types)? Or do you mean other problems than leaking, do you have an example?
- UncleMeat 7y agoEven though cpp11 has been around a while, there is a ton of code that isn't using smart pointers everywhere. This makes exception handling messy, especially with partially constructed objects.
- kibwen 7y agoRust's informal spec actually does guarantee return-value optimization (at least, it did last time I checked; the perils of being informal) so that data returned by-value is constructed in-place in the stack of its caller, but the wrinkle is that it has to be able to determine what data is going to be the return value; simple cases like returning a record literal directly (without first creating any intermediate values that might want to live in the return value) are easy, but if you do more complex operations in your factory function then you do have to start putting faith in the optimizer. Suggestions about adding "placement" semantics to Rust are concerned with allowing the user to write code guaranteeing such behavior.
- Thorrez 7y agoIs the spec specific about which cases it's required and which cases it's optional? Because simply saying "it's required if the compiler can determine it" is quite open ended.
- slaymaker1907 7y agoIt’s not that complicated. Just think of it as the compiler transforming a function “fn new(x: arg) -> Foo” to “fn new(x: arg, result: &mut Foo)”. In practice, it is fancier than this since it isn’t passing Foo as a pointer, but that is a small difference. You are not allowed to read this object directly obviously, but you effectively give it a value by the return statement so you can replace the return with an assignment. Then, the compiler really doesn’t need to do anything fancy and can just apply transitivity to get rid of most unnecessary copies. The only time this gets really tricky is when you want to use this shadow memory directly for some complex initialization since it can be overwritten arbitrarily. Personally, I don’t think this is really necessary as an an addition to Rust since I don’t think it results in much savings most of the time and you can emulate this pretty easily by writing the second form directly and forcing callers to pass an empty version of the object directly. There is a potential extra cost of an additional pointer, but if your function is that sensitive it should probably be inlined or written in assembly.
- matklad 7y agoThere are cases where copy elision doesn't work (don't know how important is that in practice). One representative example would be something like this (I think this can't use copy-elision, but I am not an expert in C++, please correct me if I am wrong!) void f(std::vector<Foo>& xs) { xs.emplace_back(arg1, arg2); } Here, the semantics is that xs is resized first and then the constructor for Foo(arg1, arg2) is called. So, if resizing xs throws, no Foo is constructed at all. If we replace emplacement with push_back and a factory function, then the semantics becomes 1) construct Foo 2) extend the vector 3) copy/move Foo into the vector.
- Thorrez 7y agoI don't understand the emplace_back example. I don't see any copies (or moves) happening there at all, so yes, no copy elision can happen. But it doesn't make sense to want copy elision when you already have no copies. But yes, I don't think push_back can do full copy elision, but I don't think there's a real need, because it can use move instead of copying.
- kbwt 7y agoIn your example, how do I embed a Foo in my FooAndBar object?