c++

"const std::string &getStr" vs "std::string getStr"


Can someone explain in simple words

why a developer did the following

const std::string &getStr() const
{
    return m_Str;
}

instead of

std::string getStr() const
{
    return m_Str;
}

Solution

  • That's two separate things. The second example returns a copy of the member variable m_Str, the first one a constant reference to the same variable. The difference is quite noticeable, if you take a look at the caller of this method. Imagine another method setStr that changes the member variable. Now take a look at the following code:

    instance.setStr("Foo");
    const std::string& str = instance.getStr();
    instance.setStr("Bar");
    std::cout << str << std::endl; // Prints "Bar".
    

    Whereas if you return a copy, the output will be different:

    instance.setStr("Foo");
    std::string str = instance.getStr();
    instance.setStr("Bar");
    std::cout << str << std::endl; // Prints "Foo".
    

    Note that the caller could still choose to create a copy in the first example:

    instance.setStr("Foo");
    //const std::string& str = instance.getStr();
    std::string str = instance.getStr(); // Copy instead of reference.
    instance.setStr("Bar");
    std::cout << str << std::endl; // Prints "Foo".
    

    Here, a new string variable is defined and gets assigned a (lvalue) reference, which calls the copy assignment operator (std::string& operator=(const std::string&)) and creates a copy.

    The other way around would not be possible, i.e., the caller cannot acquire a reference from a returned (temporary) value. This is the key semantic difference you have to think about when deciding how to pass around objects.

    In general for complex types you should prefer returning by const reference over a copy, as this involves no heap allocation. Prevent passing objects by non-const reference if you don't have to. For trivial types, returning by copy can be (and generally is) more efficient, as it allows for return value optimization (RVO).

    When designing your types, try designing them as trivial types first (that is, all constructors, assignment operators and the destructor are defaulted; if you don't provide any of those, this is the default, but be careful, as providing at least one can prevent the others from being implied!). This is not generally possible though. If your type manages a resource (such as a heap-allocated character array in case of std::string), they often require non-trivial initialization. In this case, prefer passing them as const references instead. Only if you have to enforce copy semantics (i.e., the returned value above should remain constant for all callers), pass the object around by value - it's less efficient, but as mentioned above, there's a semantic difference!

    Also note that there is also move semantics, which deals with ownership. I wont go into detail here, but generally you want to move whenever you want to transfer an object to another scope, e.g., when passing around a temporary. Again, this generally also only applies to complex types.