construction helper make_XYZ allowing RVO and type deduction even if XZY has noncopy constraint
Asked Answered
D

2

15

UPDATE1: C++17 added type deduction for constructors - which does not imply that the free function is an inferior solution.

UPDATE2: C++17 added guaranteed copy elision (the copy does not even take place conceptually). So with C++17 my code actually works and with optimal performance. But Martinho's code using brace initialisation for the return value is still the cleaner solution I believe. But checkout this answer from Barry and the comment from T.C.

OLD POST: Type deduction does not work for constructors (at least until and including C++11). The common solution is to rely on RVO (Return Value Optimisation) and write a make_XYZ template function that forwards its parameters to the constructor. An example is std::make_tuple.

Any template acrobat who knows a workaround to make this work when nocopy policy is in the way? A valid solution must still allow RVO to happen.

Additionally, will the requirement for any make_XYZ disappear with C++14?

#include <iostream>

template <typename T>
struct XYZ
{
    // remove following two lines to make the code compile
    XYZ (XYZ const & rhs) = delete; 
    XYZ (XYZ && rhs) = delete; 
    T i;
    XYZ (T i):i(i)
    {
    }
};

template <typename T>
XYZ<T> make_XYZ (T && i)
{
    return XYZ<T>(std::forward<T>(i));
}

int main ()
{
    auto x = make_XYZ(1);
    std::cout << x.i << std::endl;
}
Diomedes answered 17/10, 2013 at 12:48 Comment(10)
What if you change the return type of make_XYZ to const-reference? I'm not sure if you'll also need to change to auto const& at the call site.Cree
@JohnZwinck, no, that would crashDiomedes
Are you sure? There are special rules for lifetime promotion of the referent of const references. You can read more about that here: #2615662Cree
@JohnZwinck: Those don't allow you to magically keep function-local variables alive. I repeat: ref-to-const does not allow you to use refs to returned locals.Rettarettig
Yeah, you're right, that won't help here. Sigh, C++. :)Cree
If the ctor is not explicit, you may use a braced-init-list directly in the return statement. Johannes Schaub has suggested this approach here.Casto
@JohnZwinck, I take back my comment about the crash, sorry. Even DyPs and Martinhos answers are the right ones, you are actually making a good point here.Diomedes
@Patrick you can't return a reference to a local variable or temporary. It dies when the function ends. It might or might not crash, for such is the nature of undefined behaviour.Wittman
@R.MartinhoFernandes, the auto const & solution is clearly very suboptimal and a constref does not even work for me. However, why should this answer not apply here? Seems I am still missing something (something that was clear to me when I knew less).Diomedes
The lifetime extension only applies if you bind a temporary directly to the reference. It is not transitive, i.e., if you bind a new reference to that same reference, you don't get another lifetime extension. If you return by reference, binding another reference to that one won't make the function local live any longer. For T f(); T const& r = f(); it works because f() creates a temporary and it is bound to r and lifetime extended. But for T const& f(); T const& r = f();, it doesn't because f() gives a reference not a temporary to extend.Wittman
W
22

If there is a non-explicit constructor, it is indeed possible to return a non-copiable and non-movable type by value. See live example: http://coliru.stacked-crooked.com/a/89ef9d3115924558.

template <typename T>
XYZ<T> make_XYZ (T && i)
{
    return { std::forward<T>(i) };
}

The tricky bit here is that { ... } is not constructing a temporary and moving it to the return value. It is directly initialising the return value. There is no copy nor move, and that is irrelevant of whether any optimisation applies (it wouldn't compile if it required an optimisation to work).

However, since the type is not copyable nor movable, you will not be able to store it in a local variable by value. You can, however, use the old temporary lifetime extension trick to hold it:

auto&& x = make_XYZ(1);
Wittman answered 17/10, 2013 at 13:26 Comment(6)
+1 phantastic! I thought XYZ<> would at least have to allow for move constructor or move assignment, but this does it without either (nor copy constructor or copy assingment).Jeep
I know this is against the commenting conventions. But I can not hide it. I am impressed!Diomedes
From the question: "A valid solution must still allow RVO to happen!" -- this answer is why arbitrary restrictions on the answers in the question should basically be ignored, and why SO asks for practical problems. :)Bringingup
This is really brilliant! Unfortunately, there's a case (probably rare) where it doesn't work: when XYZ<T> has a constructor that takes a std::initializer_list<T> (in which case this constructor steals the call to XYZ(T)). My workaround follows: create struct tag {};' then add this constructor XYZ(tag, T i) : XYZ(i) {}` (important: it's (i) and not {i}) and, in make_XYZ, do return { tag{}, std::forward<T>(i) };. Maybe it's worth making tag and the delegating constructor private members of XYZ<T> and make_XYZ<T> a friend. What do you think? (Obrigado).Boer
@CassioNeri yes, that sounds like a solid plan.Wittman
I just want to share this related link regarding copy initializationDiomedes
A
1

RVO is only ever an optimisation; the copy/move has to be available for use when returning an object (temporary or named) from a function.

I'd suggest using the make_XYZ only in a nonevaluated context, using decltype:

#include <utility>

struct noncopy {
    noncopy() {}
    noncopy(noncopy &&) = delete;
    noncopy(const noncopy &) = delete;
};

template<class T1, class T2>
struct noncopy_pair: public std::pair<T1, T2>, private noncopy {
    using std::pair<T1, T2>::pair;
};

template<class T1, class T2>
noncopy_pair<T1, T2> make_noncopy_pair(T1 &&t, T2 &&u);

int main() {
    auto &&x = decltype(make_noncopy_pair(1, 'c'))(1, 'c');
}

Unfortunately you have to repeat your arguments, but you can use a macro to work around this (and the macro is at least guaranteed safe, as no argument is evaluated more than once).

Amye answered 17/10, 2013 at 13:28 Comment(4)
Unfortunately, you have been proven wrong by Martinho: copy/move does not have to be available!. bad luck.Jeep
@Jeep copy/move has to be available for RVO; Martinho's solution doesn't use RVO.Amye
Yes, but your answer can be read to mean that copy/move has to be available for a solution to the OP's problem. And that's how I read it (and still do).Jeep
@Jeep I've tried to clarify my answer; the key to the new syntax is that it doesn't involve returning an actual object from the function.Amye

© 2022 - 2024 — McMap. All rights reserved.