Question
I heard a recent talk by Herb Sutter suggesting that the usual reasons for passing std::vector and std::string as const& are mostly gone. He implied that a function like this is often preferable today:
std::string do_something(std::string inval)
{
std::string return_val;
// ... do stuff ...
return return_val;
}
I understand that return_val is returned as an rvalue, so move semantics can make the return inexpensive. However, inval is still larger than a reference, which is typically implemented as a pointer. A std::string usually contains multiple members, such as a heap pointer and storage for short string optimization. Because of that, it seems like passing by const std::string& should still be a good idea.
Why might passing std::string by value be preferable in modern C++? In what situations is pass-by-value better than const std::string&, and what role do move semantics and copy elision play here?
Short Answer
By the end of this page, you will understand why modern C++ sometimes prefers passing std::string or std::vector by value instead of const&. You will learn the trade-offs, how move semantics help, when pass-by-value is efficient, and when const& is still the better choice.
Concept
In older C++, passing large objects like std::string by const& was the default advice because copying them could be expensive. A reference is small, avoids copying, and lets the function read the original object safely.
Modern C++ changes the picture because of move semantics.
When you pass an object by value, the function receives its own local copy:
void f(std::string s);
That sounds expensive at first, but the real cost depends on what the caller passes:
- If the caller passes an lvalue (a named variable),
sis copied. - If the caller passes an rvalue (a temporary or moved object),
scan be moved into the function.
Moving a std::string is usually much cheaper than copying it, because the internal buffer can often be transferred rather than duplicated.
This leads to an important design idea:
- If the function only reads the argument,
const std::string&is often still best. - If the function needs to make its own copy anyway, then taking by value can be better.
Why? Because with pass-by-value, you pay:
- one copy for lvalues, or
- one move for rvalues.
Mental Model
Think of a function parameter as receiving a package.
- Passing by
const&is like giving the function a view of your package through a window. The function can inspect it, but it does not own it. - Passing by value is like handing over a package of its own. The function now owns that package and can keep it, change it, or move parts of it elsewhere.
Now imagine the function needs its own package anyway.
If you pass by const&, the function looks through the window first and then says, "Actually, I need my own package," so it makes a copy.
If you pass by value, the function already has its own package. And if the caller gave it a temporary package, ownership can be transferred cheaply instead of copied.
So pass-by-value is often best when the function needs ownership. const& is best when the function only needs to look.
Syntax and Examples
The two main forms are:
void read_only(const std::string& s);
void takes_ownership(std::string s);
Example 1: Read-only parameter
#include <iostream>
#include <string>
void print_name(const std::string& name) {
std::cout << name << '\n';
}
Use const std::string& when:
- the function only reads the string
- the function does not need its own copy
- you want to avoid unnecessary copying
Example 2: Function stores the argument
#include <string>
#include <utility>
class User {
std::string name;
public:
{
name = std::(new_name);
}
};
Step by Step Execution
Consider this code:
#include <string>
#include <utility>
class User {
std::string name;
public:
void set_name(std::string new_name) {
name = std::move(new_name);
}
};
Now trace two calls.
Case 1: Passing an lvalue
std::string s = "Alice";
User u;
u.set_name(s);
Step by step:
sis a named variable, so it is an lvalue.- The parameter
new_nameis constructed by copyings. - Inside the function,
name = std::move(new_name);moves the contents fromnew_nameintoname. new_nameis left in a valid but unspecified moved-from state.- The function ends, and
new_nameis destroyed.
Real World Use Cases
Setter functions
A very common example is assigning into a class member:
void set_title(std::string title) {
this->title = std::move(title);
}
This is popular in application code because it is simple and handles both lvalues and rvalues well.
Constructors that store strings or vectors
class File {
std::string path;
public:
File(std::string p) : path(std::move(p)) {}
};
If callers already have a temporary or moved string, this can be efficient.
Data transfer objects
When building request or response objects, fields are often accepted by value and then moved into members.
Parsing and transformation pipelines
If a function needs to take input text, modify it, and return a new owned value, pass-by-value may fit naturally:
std::string normalize(std::string text) {
// modify text in place
return text;
}
The caller can pass a temporary cheaply, and the function owns a modifiable string.
APIs that consume ownership
Real Codebase Usage
In real C++ codebases, developers usually choose parameter style based on intent.
Common pattern: read-only input
bool is_valid_email(const std::string& email);
This is best when the function only inspects the string.
Common pattern: sink parameter
A sink parameter is one the function is expected to take ownership of.
void save_message(std::string message) {
stored_message = std::move(message);
}
This is a common modern pattern.
Common pattern: constructor/member initialization
class Config {
std::string filename;
public:
Config(std::string f) : filename(std::move(f)) {}
};
This reduces overload clutter.
Guard clauses and validation
Functions may take by value, validate, then store:
void {
(username.()) {
std::();
}
->username = std::(username);
}
Common Mistakes
Mistake 1: Assuming pass-by-value is always faster
It is not always faster.
If a function only reads a string, this is usually unnecessary work:
void print_text(std::string text) {
std::cout << text;
}
Better:
void print_text(const std::string& text) {
std::cout << text;
}
Mistake 2: Forgetting to move from the by-value parameter when storing it
void set_name(std::string name) {
this->name = name; // copies again
}
Better:
void set_name(std::string name) {
this->name = std::move(name);
}
Mistake 3: Using std::move on a const&
Comparisons
| Situation | const std::string& | std::string by value |
|---|---|---|
| Function only reads argument | Usually best | Usually unnecessary copy/move |
| Function stores its own copy | May require copy inside function | Often a good choice |
| Caller passes lvalue | No parameter copy | Copies into parameter |
| Caller passes rvalue | Cannot directly consume it for moving if only const& | Can move into parameter |
| Need one simple API instead of overloads | May need const& + && overloads | One function often handles both |
| Function modifies local copy | Must create copy manually | Already has owned local copy |
Cheat Sheet
// Read only
void f(const std::string& s);
// Takes ownership / stores / modifies local copy
void f(std::string s);
// Typical sink pattern
void set_name(std::string s) {
name = std::move(s);
}
Quick rules
- Use
const T&when:- the function only reads the argument
- you do not need a copy
- Use
Tby value when:- the function needs its own copy anyway
- the function will store the value
- the function will modify the local copy
- you want one function that works well for both lvalues and rvalues
Key ideas
- lvalue passed to by-value parameter → copy
- rvalue passed to by-value parameter → move
const&avoids parameter copyconst&cannot usually be moved from meaningfully- returning by value is usually efficient in modern C++
Watch out
- By-value is not automatically faster
FAQ
Is passing std::string by value always better in modern C++?
No. It is usually better only when the function needs to own, store, or modify its own copy.
When should I still use const std::string&?
Use it when the function only reads the string and does not need to keep a copy.
Why does pass-by-value help with rvalues?
Because temporaries and moved objects can initialize the parameter using move semantics, which is often cheaper than copying.
Why not always write both const& and && overloads?
You can, and sometimes performance-critical code does. But pass-by-value often gives similar benefits with simpler code.
Does returning a std::string by value cause a copy?
Usually not in the expensive sense. Modern C++ uses copy elision and move semantics to make returning by value efficient.
Is moving a std::string always cheap?
Usually cheap, but not always free. Implementation details like small string optimization can affect the exact cost.
What did Herb Sutter likely mean by saying the old reasons are largely gone?
He likely meant that modern C++ lets you design simpler APIs using value parameters when ownership is needed, because moves make that pattern efficient in many common cases.
Mini Project
Description
Build a small Profile class that stores a username and a bio. The project demonstrates when passing std::string by value is useful: the class will accept incoming strings, validate or normalize them, and then move them into member variables.
Goal
Create a class that accepts text input efficiently and stores owned copies using the pass-by-value plus std::move pattern.
Requirements
- Create a
Profileclass withusernameandbiostring members. - Add setter functions that take
std::stringby value. - Validate that
usernameis not empty. - Normalize
usernameto lowercase before storing it. - Print the final profile data in
main().
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.