initializing a non-copyable member (or other object) in-place from a factory function
Asked Answered
B

2

21

A class must have a valid copy or move constructor for any of this syntax to be legal:

C x = factory();
C y( factory() );
C z{ factory() };

In C++03 it was fairly common to rely on copy elision to prevent the compiler from touching the copy constructor. Every class has a valid copy constructor signature regardless of whether a definition exists.

In C++11 a non-copyable type should define C( C const & ) = delete;, rendering any reference to the function invalid regardless of use (same for non-moveable). (C++11 §8.4.3/2). GCC, for one, will complain when trying to return such an object by value. Copy elision ceases to help.

Fortunately, we also have new syntax to express intent instead of relying on a loophole. The factory function can return a braced-init-list to construct the result temporary in-place:

C factory() {
    return { arg1, 2, "arg3" }; // calls C::C( whatever ), no copy
}

Edit: If there's any doubt, this return statement is parsed as follows:

  1. 6.6.3/2: "A return statement with a braced-init-list initializes the object or reference to be returned from the function by copy-list-initialization (8.5.4) from the specified initializer list."
  2. 8.5.4/1: "list-initialization in a copy-initialization context is called copy-list-initialization." ¶3: "if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution (13.3, 13.3.1.7)."

Do not be misled by the name copy-list-initialization. 8.5:

13: The form of initialization (using parentheses or =) is generally insignificant, but does matter when the initializer or the entity being initialized has a class type; see below. If the entity being initialized does not have class type, the expression-list in a parenthesized initializer shall be a single expression.

14: The initialization that occurs in the form T x = a; as well as in argument passing, function return, throwing an exception (15.1), handling an exception (15.3), and aggregate member initialization (8.5.1) is called copy-initialization.

Both copy-initialization and its alternative, direct-initialization, always defer to list-initialization when the initializer is a braced-init-list. There is no semantic effect in adding the =, which is one reason list-initialization is informally called uniform initialization.

There are differences: direct-initialization may invoke an explicit constructor, unlike copy-initialization. Copy-initialization initializes a temporary and copies it to initialize the object, when converting.

The specification of copy-list-initialization for return { list } statements merely specifies the exact equivalent syntax to be temp T = { list };, where = denotes copy-initialization. It does not immediately imply that a copy constructor is invoked.

-- End edit.


The function result can then be received into an rvalue reference to prevent copying the temporary to a local:

C && x = factory(); // applies to other initialization syntax

The question is, how to initialize a nonstatic member from a factory function returning non-copyable, non-moveable type? The reference trick doesn't work because a reference member doesn't extend the lifetime of a temporary.

Note, I'm not considering aggregate-initialization. This is about defining a constructor.

Banks answered 17/6, 2012 at 7:20 Comment(8)
list initialization is not the right tool to copy objects. As an example, consider struct CounterExample { }; CounterExample ce; CounterExample ce1{ ce }; /* ill-formed */.Lorislorita
@JohannesSchaub-litb Fair nuff, but I didn't say to do it, I said that there is a precondition to do it.Banks
Then I would like to break the statement that there is a valid copy or move constructor required. Consider struct CounterExample { CounterExample(CounterExample const&) = delete; int a; operator int() { return 42; } }; CounterExample factory() { return {}; } CounterExample c{factory()}; /* fine! */ Lorislorita
@JohannesSchaub-litb Actually I was hoping you could tell me if this is a hole in the language. There was some discussion between Luc and me in chat. It seems that this is almost the only case where a return value object would need to be preserved somewhere other than the stack, which would have ABI implications. But they can also be preserved by being bound to namespace-scope/static references. So I'm puzzled whether this is a semantic necessity or a syntactic omission.Banks
I don't see GCC being correct. The type is an aggregate, so according to C++11 it should be accepted (clang does accept it)Lorislorita
Would that GCC bug be what is causing my problem in this situation? I can successfully compile the following function: T && make_T() { return std::move(T(constructor_argument)); }. The std::move and the && are both required to make it compile. However, I cannot compile when I try to do either member_variable(make_T()) or member_variable(std::move(make_T())). Is this where GCC is incorrect? My code makes use of C++11 features that clang doesn't support, so I can't check that way.Marvellamarvellous
@DavidStone Johannes is referring specifically to Aggregate types, which probably doesn't apply to you. You are using direct-initialization with the parens, not list-initialization with braces. Also, if I recall, Johannes' counterexample applies to local variables but not members.Banks
@JohannesSchaub-litb: C++14 fixed your empty-struct case to be a copy after all.Diathermy
D
2

On your main question:

The question is, how to initialize a nonstatic member from a factory function returning non-copyable, non-moveable type?

You don't.

Your problem is that you are trying to conflate two things: how the return value is generated and how the return value is used at the call site. These two things don't connect to each other. Remember: the definition of a function cannot affect how it is used (in terms of language), since that definition is not necessarily available to the compiler. Therefore, C++ does not allow the way the return value was generated to affect anything (outside of elision, which is an optimization, not a language requirement).

To put it another way, this:

C c = {...};

Is different from this:

C c = [&]() -> C {return {...};}()

You have a function which returns a type by value. It is returning a prvalue expression of type C. If you want to store this value, thus giving it a name, you have exactly two options:

  1. Store it as a const& or &&. This will extend the lifetime of the temporary to the lifetime of the control block. You can't do that with member variables; it can only be done with automatic variables in functions.

  2. Copy/move it into a value. You can do this with a member variable, but it obviously requires the type to be copyable or moveable.

These are the only options C++ makes available to you if you want to store a prvalue expression. So you can either make the type moveable or return a freshly allocated pointer to memory and store that instead of a value.

This limitation is a big part of the reason why moving was created in the first place: to be able to pass things by value and avoid expensive copies. The language couldn't be changed to force elision of return values. So instead, they reduced the cost in many cases.

Deshawndesi answered 17/6, 2012 at 17:42 Comment(11)
Please reference (or otherwise back) a claim I'm wrong, do not simply assert "wrong" and move on. I'll elaborate that point in the question, then downvote this.Banks
I didn't suggest to use case 2, I suggest C && x = factory();Banks
@Potatoswatter: True. I didn't realize that before. However, I would also point out that the whole braced-init-list thing doesn't do what you said: "new syntax to express intent instead of relying on a loophole." It is just syntactic sugar; the constructing of the temporary in place would happen if it were an explicit constructor call rather than a braced-init-list. More importantly, it doesn't change the primary thrust of the answer: you can't store a non-copyable, non-movable temporary value as a non-static member of a class. I'll clean it up though, to clarify this point.Deshawndesi
No, an explicit constructor call would result in move construction. return {x}; is by no means translated to return C{x};, the spec handles the cases completely differently. If you disagree, you'll need to reference the assertion. Aside from that, you're just reiterating the given alternatives and jumping to the conclusion that none remain. Perhaps it can't be done directly using a member of type C, but it hardly seems appropriate to jump to the conclusion that the problem is intractable.Banks
@Potatoswatter: I'm not jumping to any conclusion; there are only so many options available from C++. You have an xvalue returned from a function; it doesn't matter how you generated this xvalue because the local code can't tell the difference (unless the function definition were available). You can either store that in a reference (const& or &&), thus extending its lifetime, or you can copy/move it into an actual object. These are all of the options C++ gives you, period. You might return a pointer and stick it into a unique_ptr, but you wouldn't be returning the class by value.Deshawndesi
@Potatoswatter: BTW, I just realized that the copy you're talking about is the copy into the return value, not the copy returned from the function into the value the user is capturing it into. OK, yes, uniform initialization removes that need. But as I have now pointed out, how the return value is created doesn't change the requirements for storing that return value.Deshawndesi
A return value object from a function is a prvalue, like any temporary. I've been talking about both copies, which is why they're addressed separately in the question. I'm not going to suggest more corrections here. Simply put, I'm not looking for a reason that this can't be done, I'm looking for a minimal workaround. Right now it occurs to me that a factory returning forward_as_tuple and constructor in C accepting such a tuple could be a solution. Energy is better spent trying it out than looking up Standard terminology for you.Banks
@Potatoswatter: But then it wouldn't be a factory function, because it's not creating the object. Workarounds mean that you're changing the parameters of the problem in some way, but you never stated what changes would be acceptable in your circumstances. I suggested using pointers; was that acceptable? Your solution makes the factory function not actually a factory, relying instead on a public constructor. How were we supposed to know that this solution would be acceptable? Generally, the point of a factory would be to ensure that all construction goes through known paths.Deshawndesi
let us continue this discussion in chatDeshawndesi
Sorry, when I formulated the question I was hoping that such a change wouldn't be necessary. I've removed the downvote since it looks like all the errors are gone. (Although I'm not sure the part before "You have a function…" is useful.) Maybe I'll accept it; that would be the fair thing to do for you. But for the sake of someone else visiting the page, a workaround would be more useful.Banks
Finally accepting this. I'm still not sure about "The language couldn't be changed to force elision of return values," but it's likely true in terms of preserving ABIs with callee-allocated return values. Still, it makes way more sense for the caller to pass a pointer to where the return value should be constructed. (Besides the stack pointer.) A survey of ABIs would be nice…Banks
D
1

Issues like this were among the prime motivations for the change in C++17 to allow these initializations (and exclude the copies from the language, not merely as an optimization).

Diathermy answered 18/9, 2017 at 1:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.