4

Is there any method that can pass the ownership of an object created in the function on the stack memory to the outside of the function without using copy construction?

Usually, compiler will automatically call destruction to the object on the stack of a function. Therefore, if we want to create an object of a class(maybe with some specific parameters), how can we avoid wasting lots of resources copying from temp objects?

Here is one common case:

while(...){
    vectors.push_back(createObject( parameter ));
}

So when we want to create objects in a iteration with some parameters, and push them into a vector, normal value passed way will take a lot of time copying objects. I don't want to use pointer and new objects on the heap memory as user are likely to forget delete them and consequently cause memory leak.

Well, smart pointer maybe a solution. But..less elegent, I think. hhhh

Is there any way of applying rvalue reference and move semantics to solve this problem?

fateflame
  • 157
  • 1
  • 8
  • 1
    If you just return an object by value, the compiler will most likely use [return value optimizations like copy-elision](http://stackoverflow.com/questions/12953127/what-are-copy-elision-and-return-value-optimization). – Some programmer dude Aug 16 '17 at 11:25
  • [copy elision](http://en.cppreference.com/w/cpp/language/copy_elision) is a thing. The copies are not a problem. – Jesper Juhl Aug 16 '17 at 11:26
  • Your fear of memory leaks has nothing to do with your problem. Memory managment is an implementation detail and is coverd by RAII. – knivil Aug 16 '17 at 11:49
  • 1
    if object is cheap to move, current code is ok, else you probably want to wrap it inside a smart pointer. – Jarod42 Aug 16 '17 at 11:53
  • Well, if I create objects on heap and return their pointers, it means I must manually `delete` them in another function. It's not a safe implement when I cooperate with a group of people as they are much more like to forget it, I think.@knivil – fateflame Aug 17 '17 at 07:02
  • @Some programmer dude. Thanks a lot! But as the answer in the question points out, how and when this elision happens are decided by the compiler. So is there some methods to **explicitly** make the elision. (Just like why c++11 give keywords `explicit` and `delete` in construction declaration.) – fateflame Aug 17 '17 at 07:34
  • @fateflame find a c++17 compiler. That standard has made copy elision *mandatory* in a bunch of cases – Caleth Aug 17 '17 at 08:14
  • @Caleth Er.... well, thank you. I guess I can image one hhhhhhhhh – fateflame Aug 17 '17 at 08:53
  • To rephrase: the part of the standard that deals with (11 & 14 allowed) (17 required) copy elision is based on what compilers were already doing. That's your description of when it occurs, and the "it's becoming mandatory" is explanation of why there isn't a keyword for "elide this copy" – Caleth Aug 17 '17 at 08:57

2 Answers2

4

Typically, returning an object by value will not copy the object, as the compiler should do a (named) return value optimization and thereby elide the copy.

With this optimization, the space for the returned object is allocated from the calling context (outer stack frame) and the object is constructed directly there.

In your example, the compiler will allocate space for the object in the context where createObject() is called. As this context is an (unnamed) parameter to the std::vector<T>.push_back() member function, this works as an rvalue reference, so the push_back() by-value will consume this object by moving (instead of copying) it into the vector. This is possible since if the generated objects are movable. Otherwise, a copy will occur.

In sum, each object will be created and then moved (if moveable) into the vector.

Here is a sample code that shows this in more detail:

#include <iostream>
#include <string>
#include <vector>

using Params = std::vector<std::string>;

class Object
{
public:
    Object() = default;
    Object(const std::string& s) : s_{s}
    {
        std::cout << "ctor: " << s_ << std::endl;
    }
    ~Object()
    {
        std::cout << "dtor: " << s_ << std::endl;
    }

    // Explicitly no copy constructor!
    Object(const Object& other) = delete;

    Object(Object&& other)
    {
        std::swap(s_, other.s_);
        std::cout << "move: traded '" << s_ << "' for '" << other.s_ << "'" << std::endl;
    }

    Object& operator=(Object other)
    {
        std::swap(s_, other.s_);
        std::cout << "assign: " << s_ << std::endl;
        return *this;
    }

private:
    std::string s_;
};


using Objects = std::vector<Object>;

Object createObject(const std::string& s)
{
    Object o{s};
    return o;
}

int main ()
{
    Objects v;
    v.reserve(4);  // avoid moves, if initial capacity is too small
    std::cout << "capacity(v): " << v.capacity() << std::endl;

    Params ps = { "a", "bb", "ccc", "dddd" };

    for (auto p : ps) {
        v.push_back(createObject(p));
    }

    return 0;
}

Note that the class Object explicitly forbids copying. But for this to work, the move constructur must be available.

A detailed summary on when copy elision can (or will) happen is available here.

King Thrushbeard
  • 869
  • 10
  • 16
1

Move semantics and copy elison of vectors should mean the elements of the local std::vector are in fact passed out of the object and into your local variable.

Crudely you can expect the move constructor of std::vector to be something like:

//This is not real code...
vector::vector(vector&& tomove){
    elems=tomove.elems; //cheap transfer of elements - no copying of objects.
    len=tomove.elems;
    cap=tomove.cap;

    tomove.elems=nullptr;
    tomove.len=0;
    tomove.cap=0;
}

Execute this code and notice the minimum number of objects are constructed and destructed.

#include <iostream>
#include <vector>


class Heavy{
    public:
     Heavy(){std::cout<< "Heavy construction\n";}
     Heavy(const Heavy&){std::cout<< "Heavy copy construction\n";}
     Heavy(Heavy&&){std::cout<< "Heavy move construction\n";}
     ~Heavy(){std::cout<< "Heavy destruction\n";}

};


std::vector<Heavy> build(size_t size){

    std::vector<Heavy> result;
    result.reserve(size);
    for(size_t i=0;i<size;++i){
        result.emplace_back();
    }
    return result;
}


int main() {

    std::vector<Heavy> local=build(5);
    std::cout<<local.size()<<std::endl;

    return 0;
}

Move semantics and copy elison tend to take care of this problem C++11 onwards.

Expected output:

Heavy construction
Heavy construction
Heavy construction
Heavy construction
Heavy construction
5
Heavy destruction
Heavy destruction
Heavy destruction
Heavy destruction
Heavy destruction

Notice that I reserved capacity in the vector before filling it and used emplace_back to construct the objects straight into the vector.

You don't have to get the value passed to reserve exactly right as the vector will grow to accommodate values but it that will eventually lead to a re-allocation and move of all the elements which may be costly depending on whether you or the compiler implemented an efficient move constructor.

Persixty
  • 8,165
  • 2
  • 13
  • 35