c++c++17return-value

When does assigning the return value result in a copy?


There are six lines in int main below where a variable is being created and initialized; in which (if any) of these scenarios does a copy get created for the assignment?

Additionally, in the case of sixthMap, does/could anything weird happen, since we're using automatic deduction with an ampersand but the function it's calling also returns a reference?

std::unordered_map<std::string, std::vector<std::string>>
createStdObject()
{
    std::unordered_map<std::string, std::vector> newUnorderedMap
    {
        {"A", {"first", "vector", "in", "map"}, 
        {"B", {"second", "vector", "in", "map"}
    };

    return newUnorderedMap;
}

std::unordered_map<std::string, std::vector<std::string>>&
createStdObjectWithReference()
{
    std::unordered_map<std::string, std::vector> newUnorderedMap
    {
        {"A", {"first", "vector", "in", "map"}, 
        {"B", {"second", "vector", "in", "map"}
    };

    return newUnorderedMap;
}

int main()
{
    std::unordered_map<std::string, std::vector> firstMap = newUnorderedMap();
    std::unordered_map<std::string, std::vector> secondMap = createStdObjectWithReference();
    
    const auto thirdMap = newUnorderedMap();
    const auto& fourthMap = newUnorderedMap();

    const auto fifthMap = createStdObjectWithReference();
    const auto& sixthMap = createStdObjectWithReference();
    
}

What I would guess - based on my limited knowledge - is:

  1. firstMap and thirdMap could be copies of the return value of newUnorderedMap()
  2. All of the others would not result in copies, but a move/swap
  3. The ampersand in the auto declaration of sixthMap would essentially be ignored, and sixthMap would be identical to fifthMap in terms of type and behaviour

On the other hand, it is obvious at compile time itself that none of the return values actually need to be copied, and as such I see no reason why they shouldn't all result in a move assignment


Solution

  • Ignoring that your code has a lot of, what I assume are, typing errors, what you fail to understand is one of the most fundamental concepts in C++: object lifetime. To explain it briefly in an unprecise way (for the sake of understanding):

    Every object created on the stack (i.e. locally) has its scope (you can recognize a scope by curly braces). Once that scope is exited, all the objects that were created in that scope are destroyed. Furthermore, each function is also a scope, which means that each object created in a function stops existing once the end of the function is reached. Therefore, the only way for objects created inside a function to "outlive" the function is if they are copied/moved to some address external to the function.

    What might seem like an exception is this thing called copy-elision. When your function returns by value, and the returned object was created in that function, there will be no copy/move involved and it would seem like your newly created object escaped its scope. However, in reality, your object was actually created on the call site, outside of the scope of that function, while function is used only to initialize that already created object. This is what copy-elision means, roughly. It boils down to copying a pointer, but no constructor is invoked. Note, however, that the Standard does not guarantee this to happen yet, but, for all practical purposes, it is what happens.

    Therefore, the following code:

    #include <cstdio>
    class Lifetime
    {
      public:
        Lifetime() { std::puts("default ctor"); }
        Lifetime(Lifetime&&) { std::puts("move ctor"); }
        Lifetime(const Lifetime&) { std::puts("copy ctor"); }
        ~Lifetime() { std::puts("destructor"); }
    };
    
    Lifetime make_lifetime() {
        Lifetime l{}; // Lifetime object initialized
        return l; // Lifetime object not destroyed because created outside the function scope
    }
    
    const Lifetime& make_lifetime_ref() {
        Lifetime l{}; // Lifetime object created
        return l;     // Lifetime object destroyed
    }
    
    int main() {
        const auto lifetime1 = make_lifetime(); // Lifetime object created; copy ctor not called!
    
        // 'make_lifetime_ref' returns a reference to already destroyed object
        // const auto lifetime2 = make_lifetime_ref(); // oops! undefined behaviour:
    
        return 0;
    }
    

    would produce this output:

    $ default ctor
    $ destructor
    

    As you can see, there is no copying involved.

    Note that auto has nothing to do with any of this, i.e. it will not affect copying. So, in your case, the creation of firstMap, thirdMap and fourthMap would not involve copying, while the other three would cause undefined behavior. Also note that this is all relating to local variables, others have a bit different rules.