Can we return objects having a deleted/private copy/move constructor by value from a function?
Asked Answered
I

5

21

In C++03 it is impossible to return an object of a class having a private non-defined copy constructor by value:

struct A { A(int x) { ... } private: A(A const&); };

A f() {
  return A(10); // error!
  return 10;    // error too!
}

I was wondering, was this restriction lifted in C++11, making it possible to write functions having a class type return type for classes without constructors used for copy or move? I remember it could be useful to allow callers of a function use the newly returned object, but that they are not able to copy the value and store it somewhere.

Irenairene answered 28/10, 2011 at 22:46 Comment(5)
What does it mean to return an object "by value ... without copying or moving" it?Appendicle
@Appendicle to have a function definition that doesn't return void or a reference. I mean a function that returns a new class object. I will clarify.Irenairene
@Xeo : Because it's a silly question by someone who knows better. And I say that as someone who has known/respected litb before I knew SO. ;-]Lindley
@Lindley ohh how I feel the love. I uncovered the solution to this quiz below!Irenairene
@Johannes : Fair enough!Lindley
I
18

Here is how it can work

A f() {
  return { 10 };
}

This works even though A has no working copy or move constructor and no other constructor that could copy or move an A!

To make use of this feature of C++11, the constructor (taking int in this case) has to be non-explicit though.

Irenairene answered 29/10, 2011 at 12:36 Comment(10)
You and your quiz-questions. :(Clarke
It doesn't compile with g++ -std=c++0x. By the way how are you supposed to be able to call it?Hothead
Given that David's answer seems to indicate that this should still be illegal, can you please also post the relevant standardese supporting your answer?Lindley
@Lindley oops, somehow I missed these comments. This is specified at 8.5.4p3. Note that we initialize an A (the object representing the return value) by a { 10 }. There is no copy and no move involved.Irenairene
@Hothead comments about software behaving in an unexpected way are useless if you don't give version numbers. I use GCC4.7 trunk ("4.7.0 20110929") which doesn't show the behavior you describe.Irenairene
@Johannes : I see nothing in §8.5.4/3 to indicate that this is legal. The only relevant part of the paragraph seems to be "Otherwise, if T is a class type, constructors are considered." What am I missing? I.e., how is your code any different than A f() { return A(10); }?Lindley
@Lindley exactly, constructors are considered, enumerated and the best one is chosen by overload resolution and then called to construct the return value object. To see how it is different from return 10, you can compare the description of 8.5.4/3 to 8.5/16 bullet 6 subbullet 2. As will be noticed by the avid reader, return 10 does a copy of a temporary A object to another A object (the destination). And return A(10) does so too - the temporary A object that will be copied is even explicitly constructed.Irenairene
You don't need to find something that explicitly states that this is legal. There is no explicit rule that converting 3 to long is legal either, but it follows from a rule that converting an arbitrary int to a long is legal.Irenairene
Thanks for the clarification, I think I finally get it now. ;-]Lindley
Does C++17's guaranteed copy elision impact this?Asante
A
6

The restriction has not been lifted. As per the access specifier, there is a note in §12.8/32 that explains:

two-stage overload resolution must be performed regardless of whether copy elision will occur. It determines the constructor to be called if elision is not performed, and the selected constructor must be accessible even if the call is elided.

As of the deleted copy/move constructors §8.4.3/2 states that

A program that refers to a deleted function implicitly or explicitly, other than to declare it, is ill-formed. [ Note: This includes calling the function implicitly or explicitly and forming a pointer or pointer-to-member to the function. It applies even for references in expressions that are not potentially-evaluated. If a function is overloaded, it is referenced only if the function is selected by overload resolution. — end note ]

Not sure about this particular case, but my understanding of the quote is that, if after the overload resolution in §12.8/32 the deleted copy/move constructor is selected, even if the operation is elided, that could constitute a reference to the function, and the program would be ill formed.

Anthropolatry answered 28/10, 2011 at 23:41 Comment(0)
D
5

The above code is still ill-formed in C++11. But you could add a public move constructor to A and then it would be legal:

struct A
{
    A(int x) {}
    A(A&&);
private:
    A(A const&);
};

A f() {
  return A(10); // Ok!
}
Daughtry answered 28/10, 2011 at 22:51 Comment(1)
Hi! I'm sorry, I should have repeated what I had in the title also in the question's body. Will do so now. But it's a good remark either way!Irenairene
I
2

I was wondering, was this restriction lifted in C++11?

How could it be? By returning something by value, you are by definition copying (or moving) it. And while C++ can allow that copy/move to be elided in certain circumstances, it's still copying (or moving) by the specification.

I remember it could be useful to allow callers of a function use the returned object, but that they are not able to copy the value and store it somewhere.

Yes. You get rid of the copy constructor/assignment, but allow the value to be moved. std::unique_ptr does this.

You can return a unique_ptr by value. But in doing so, you are returning an "prvalue": a temporary that is being destroyed. Therefore, if you have a function g as such:

std::unique_ptr<SomeType> g() {...}

You can do this:

std::unique_ptr<SomeType> value = g();

But not this:

std::unique_ptr<SomeType> value1 = g();
std::unique_ptr<SomeType> value2 = g();
value1 = value 2;

But this is possible:

std::unique_ptr<SomeType> value = g();
value = g();

The second line invokes the move assignment operator on value. It will delete the old pointer and move the new pointer into it, leaving the old value empty.

In this way, you can ensure that the contents of any unique_ptr is only ever stored in one place. You can't stop them from referencing it in multiple places (via pointers to unique_ptr or whatever), but there will be at most one location in memory where the actual pointer is stored.

Removing both the copy and move constructors creates an immobile object. Where it is created is where it's values stay, forever. Movement allows you to have unique ownership, but without being immobile.

Incorporating answered 28/10, 2011 at 23:8 Comment(3)
I like that you're highlighting unique_ptr. But your line that contains the comment: //Copy assignment is forbidden. is incorrect. This is a move assignment and unique_ptr allows it.Daughtry
@HowardHinnant: I thought I'd deleted that part before posting it (which is why I later show that it works), but I didn't delete all of it.Incorporating
g() is a prvalue, not an xvalue. If g were declared e.g. std::unique_ptr<SomeType>&& g();, then g() would be an xvalue. Note that for your examples, it suffices that g() be an rvalue anyway.Cale
C
0

You could probably hack together a proxy to do the trick if you really wanted, and have a converting constructor that copies the value stored within the proxy.

Something along the lines of:

template<typename T>
struct ReturnProxy {
    //This could be made private, provided appropriate frienship is granted
    ReturnProxy(T* p_) : p(p_) { }
    ReturnProxy(ReturnProxy&&) = default;

private:
    //don't want these Proxies sticking around...
    ReturnProxy(const ReturnProxy&) = delete;
    void operator =(const ReturnProxy&) = delete;
    void operator =(ReturnProxy&&) = delete;

    struct SUPER_FRIENDS { typedef T GO; };
    friend struct SUPER_FRIENDS::GO;
    unique_ptr<T> p;
};

struct Object {
    Object() : data(0) { }

    //Pseudo-copy constructor
    Object(ReturnProxy<Object>&& proxy)
      : data(proxy.p ? proxy.p->data : throw "Don't get sneaky with me \\glare") 
    {
      //steals `proxy.p` so that there isn't a second copy of this object floating around
      //shouldn't be necessary, but some men just want to watch the world burn.
      unique_ptr<Object> thief(std::move(proxy.p));
    }
private:
    int data;

    Object(const Object&) = delete;
    void operator =(const Object&) = delete;
};

ReturnProxy<Object> func() {
    return ReturnProxy(new Object);
}

int main() {
    Object o(func());
}

You could probably do the same in 03, though, using auto_ptrs. And it obviously doesn't prevent storage of the resultant Object, although it does limit you to one copy per instance.

Clarineclarinet answered 29/10, 2011 at 0:56 Comment(2)
I was trying to figure out some way to get rid of the publicly accessible move constructor from ReturnProxy, but then I realized that's the original question, just moved back a level.Clarineclarinet
Even if you did, I could just store the proxy by value using auto. auto gets to declare private types and such.Incorporating

© 2022 - 2024 — McMap. All rights reserved.