C++: What does a const reference to the return value of a function mean?

Viewed 678

Here's a snippet from my C++ code:

std::queue<std::string> get_file_names(const std::string &indir)
{
    std::queue<std::string> file_names;

    fs::recursive_directory_iterator end;
    for (fs::recursive_directory_iterator it(indir); it != end; it++) {
        const std::string &extn = it->path().extension().string();
        if (extn == ".zip") {
            const std::string &file_name = it->path().string();
            file_names.push(file_name);
        }
    }

    return file_names;
}

Is it a good practice to make every string you won't modify a const reference? I have trouble understanding how can such a reference exist in this context at all. Like the return value of it->path().string() above. How can it be assigned to a reference that can be later used outside of the scope of the function when pushed back to a vector?

I feel like it has to do something with std::move.

3 Answers

Your code:

const std::string &file_name = it->path().string();

extends the lifetime of the temporary std::string returned by std::filesystem::path::string(). Since you've marked that as const, it can't be moved into file_names, it must be copied. Assuming you want a move, you would write:

auto&& file_name = // ...
file_names.push(std::move(file_name));

Notice that std::queue has a push() overload for r-value references.

Modern C++ provides a lot of opportunities for compilers to optimize, so avoiding questions/"confusion" about dangling references (the auto&& syntax is "new" in C++11) might be a better approach:

auto file_name = // ...
file_names.push(std::move(file_name));

Writing "natural" code that "looks and behaves like the ints" is often a good approach. In the unlikely situation you find that this is really a performance bottleneck, you can revisit; write your code for clarity first.

No I wouldn't recommend to ever use const std::string&. If you want to have a viewer to a string then use std::string_view instead. std::string_view is not suitable only in the case with old C-style API where function accepts const char* as input without an option to submit its size; hopefully these APIs die out eventually. All decent API always add option to submit const char* coupled with its size as an additional option.

If you want a std::string then just use std::string or if you want to explicitly state that you don't intend to change it then just make it const std::string.

In your case as pointed by other answers, the returned object by the path's method .string() is a std::string so capturing it as const std::string& explicitly is just non-sensical.

If you stored it as std::string then at least you could've moved it into the output std::queue<std::string> file_names.

Edit: about why old C-style strings aren't good and that length should be forwarded. Please check this article

https://nee.lv/2021/02/28/How-I-cut-GTA-Online-loading-times-by-70/

Turns out GTA's loading times very crazy slow (several minutes, 5-6 on average) because it was stuck computing strlen over and over again while reading a 10mb json file.

Explanations

How can it be assigned to a reference that can be later used outside of the scope of the function when pushed back to a vector?

The variable file_names is of type std::queue<std::string> (and not std::queue<std::string&> - that is by the way not possible this way, but by using std::reference_wrapper). So it does not store "references to strings" but the actual "strings".

If you push a string reference, actually a copy of the referenced string will be pushed.


Is it a good practice to make every string you won't modify a const reference?

Regarding "best practice" consider using std::string_view in exchange for const string references (at least since C++17).


I feel like it has to do something with std::move.

It does not. In this case it has something to do with the type argument of the std::queue.

Bonus

You can store references within a (STL) container by using a reference_wrapper as type argument.

Related