Search code examples
c++c++14stdstring

How many temporary std::string object be created in the code? (Effective Modern C++ materials)


I have been studying Effective Modern C++11 and 14 recently.

I went through a part of the book located on page 171, item 25, where the author provides two code examples for the setName function.

(1)

// Option 1, universal reference
class Widget {
public:
    template<typename T>
    void setName(T &&newName)     // universal reference
    { name = std::move(newName); }// compiles, but is // bad, bad, bad!
                                  //…
private:
    std::string name;
    std::shared_ptr<SomeDataStructure> p;
};

void main() {
    Widget w;
    w.setName("Adela Novak");
}

(2)

// Option 2, lvalue reference and rvalue reference
class Widget2 {
public:
    void setName(const std::string &newName) { name = newName; }
    void setName(std::string &&newName) { name = std::move(newName); }
    //…
};

void main() {
    Widget2 w;
    w.setName("Adela Novak");
}

The author said:

With the version of setName taking a universal reference, the string literal "Adela Novak" would be passed to setName, where it would be conveyed to the assignment operator for the std::string inside w. w’s name data member would thus be assigned directly from the string literal; no temporary std::string objects would arise.

With the overloaded versions of setName, however, a temporary std::string object would be created for setName’s parameter to bind to, and this temporary std::string would then be moved into w’s data member. A call to setName would thus entail execution of one std::string constructor (to create the temporary), one std::string move assignment operator (to move newName into w.name), and one std::string destructor (to destroy the temporary).

If my understanding is correct, in both option 1 and option 2, we will create a temporary std::string object for “Adela Novak” implicitly in main() function.

For option 1, we send that temporary std::string object to setName(), and then T&& will be deduced as rvalue because “Adela Novak” is rvalue.

Hence, in option 1, we only create one temporary std::string object.

For option 2, we send that temporary std::string object to setName(). Because “Adela Novak” is rvalue, so we will pick up the rvalue overload setName() function. But, when we are going to call rvalue overload function, we have to create another temporary std::string object explicitly for newName.

Hence, in option 2, we have to create two temporary std::string objects.

My question is:

  1. Why don’t we have to create another temporary std::string object for newName in option 1?
  2. Why we have to create another temporary std::string object for newName in option 2?
  3. How to know that the member function we are going to call will create a temporary object or not?

Updated: I asked my question to ChatGPT. However, it seems like the answer ChatGPT provided not 100% author's answer...

In Option 1, when the string literal "Adela Novak" is passed to the setName() function, the type deduction of T results in T being deduced as a const char* pointer. Since T is a non-reference type, newName is declared as an rvalue reference (T&&).

When the rvalue reference newName is passed to the std::move() function, it performs a cast to an rvalue, which allows it to bind to the move constructor of the std::string class. Therefore, a temporary std::string object is created using the std::string constructor that takes a const char* argument.

This temporary std::string object is then moved to the name data member using the move assignment operator, resulting in no further copies of the string data.

So, in Option 1, one temporary std::string object is created to hold the string data, but no additional temporary objects are created.

In Option 2, when the string literal "Adela Novak" is passed to the setName() function, the compiler tries to find the best match for the argument.

The first setName() function taking a const std::string& parameter is a match because it can bind to a temporary std::string object created from the string literal. Therefore, the temporary std::string object is created using the std::string constructor that takes a const char* argument.

Next, the temporary std::string object is passed to the setName() function by reference, which triggers the copy constructor of std::string to create a copy of the temporary object.

Finally, the temporary std::string object is moved to the name data member using the move assignment operator, which results in the temporary object being destroyed.

So, in Option 2, two temporary std::string objects are created, one for holding the string data, and another one for binding to the std::string&& parameter of the overloaded setName() function.


Solution

  • The author is clear about option 1:

    [..] the string literal "Adela Novak" would be passed to setName, where it would be conveyed to the assignment operator for the std::string inside w. w’s name data member would thus be assigned directly from the string literal; no temporary std::string objects would arise.

    A string literal is not an std::string, it's a char array (const char[]). So the universal reference allows you to pass an object of any type without any conversion or temporary objects.

    As for option 2, a temporary object may or may not be created, depends on what you pass in setName. If you pass an std::string, then no temporaries will be created. If you pass a string literal, you need a temporary std::string to be created first and then pass a reference to it.

    void foo(const std::string& s) { ... }
    void foo(std::string&& s) { ... }
    
    int main() {
        std::string str(10, 'a'); // "aaaaaaaaaa"
        foo(str); // No temporary, passing a reference to 'str'
        foo("Hello"); // Creating a temporary std::string and passing a reference to the temporary
        return 0;
    }