How to Initialize a String Member
There are several ways to initialize a string member when working in C++. What way works best for your projects? Read on to find out!
Join the DZone community and get the full member experience.
Join For FreeHow do you initialize a string
member in the constructor? By using const string&
, string
value and move
, string_view
or maybe something else?
The article comes from bfilipek.com - blog about C++, new articles each Monday.
Let's have a look at possible options.
Intro
Below there's a simple class with one string
member. We'd like to initialize it.
For example:
class UserName {
std::string mName;
public:
UserName(const std::string& str) : mName(str) { }
};
As you can see a constructor is taking const std::string& str
.
You could potentially replace a constant reference with string_view
:
UserName(std::string_view sv) : mName(sv) { }
And also you can pass a string
by value and move from it:
UserName(std::string s) : mName(std::move(s)) { }
Which one alternative is better?
Analyzing the Cases
Let's now compare those alternative string passing methods in three cases: creating from a string literal, creating from lvalue
, and creating from an rvalue
reference:
// creation from a string literal
UserName u1{"John With Very Long Name"};
// creation from l-value:
std::string s1 { "Marc With Very Long Name"};
UserName u2 { s1 };
// from r-value reference
std::string s2 { "Marc With Very Long Name"};
UserName u3 { std::move(s2) };
And now we can analyze each version - with a string
reference, a string_view
, or a value. Please note that allocations/creation of s1
and s2
are not taken into account, we only look at what happens for the constructor call.
For const std::string&
:
u1
- two allocations: the first one creates a temp string and binds it to the input parameter, and then there's a copy intomName
.u2
- one allocation: we have a no-cost binding to the reference, and then there's a copy into the member variable.u3
- one allocation: we have a no-cost binding to the reference, and then there's a copy into the member variable.- You'd have to write a
ctor
taking r-value reference to skip one allocation for theu1
case, and also that could skip one copy for theu3
case (since we could move from r-value reference).
For std::string_view
:
u1
- one allocation - no copy/allocation for the input parameter, there's only one allocation whenmName
is created.u2
- one allocation - there's cheap creation of astring_view
for the argument, and then there's a copy into the member variable.u3
- one allocation - there's cheap creation of astring_view
for the argument, and then there's a copy into the member variable.- You'd also have to write a constructor taking r-value reference if you want to save one allocation in the
u3
case, as you could move fromr-value
reference. - You also have to pay attention to dangling
string_views
— if the passedstring_view
points to a deleted string object...
For std::string
:
u1
- one allocation - for the input argument and then one move into themName
. It's better than withconst std::string&
where we got two memory allocations in that case. And similar to thestring_view
approach.u2
- one allocation - we have to copy the value into the argument, and then we can move from it.u3
- no allocations, only two move operations - that's better than withstring_view
andconst string&
!
When you pass std::string
by value not only the code is simpler, there's also no need to write separate overloads for r-value
references.
The approach of passing by value is consistent with item 41 - "Consider pass by value for copyable parameters that are cheap to move and always copied" from Effective Modern C++ by Scott Meyers.
However, is std::string
cheap to move?
When String Is Short
Although the C++ Standard doesn't specify that, usually, strings are implemented with Small String Optimization (SSO); the string object contains extra space (in total it might be 24 or 32 bytes), and it can fit 15 or 22 characters without additional memory allocation. That means that moving such a string is the same as copying. And since the string is short, the copy is also fast.
Let's reconsider our example of passing by value when the string
is short:
UserName u1{"John"}; // fits in SSO buffer
std::string s1 { "Marc"}; // fits in SSO buffer
UserName u2 { s1 };
std::string s2 { "Marc"}; // fits in SSO buffer
UserName u3 { std::move(s2) };
Remember that each move is the same as copy now.
For const std::string&
:
u1
- two copies: one copy from the input string literal into a temporary string argument, then another copy into the member variable.u2
- one copy: the existing string is bound to the reference argument, and then we have one copy in the member variable.u3
- one copy:rvalue
reference is bound to the input parameter at no cost, later we have a copy in the member field.
For std::string_view
:
u1
- one copy: no copy for the input parameter, there's only one copy whenmName
is initialized.u2
- one copy: no copy for the input parameter, asstring_view
creation is fast, and then one copy into the member variable.
For std::string
:
u1
- two copies: the input argument is created from a string literal, and then there's a copy inmName
.u2
- two copies: one copy into the argument and then the second copy into the member.u3
- two copies: one copy into the argument (move means copy) and then the second copy into the member.
As you see for short strings passing by value might be "slower" when you pass some existing string, because you have two copies rather than one. On the other hand, the compiler might optimize the code better when it sees a value. What's more, short strings are cheap to copy so the potential "slowdown" might not be even visible.
A Note on Universal (Forwarding) References
There's also another alternative:
class UserName {
std::string mName;
public:
template<typename T>
UserName(T&& str) : mName(std::<T>forward(str)) { }
};
In this case, we ask the compiler to do the hard work and figure out all the proper overloads for our initialization case. It's not only working for input string arguments, but actually other types that are convertible to the member object.
For now, I'd like to stop here and not go into details. You may experiment with that idea and figure out if this is the best option for string passing. What are the pros and cons of that approach?
Some more references:
- Universal vs Forwarding References in C++ | Petr Zemek
- Universal References in C++11—Scott Meyers : Standard C++
Summary
All in all, passing by value and then moving from a string argument is the preferred solution in Modern C++. You have simple code and better performance for larger strings. There's also no risk with dangling references as in the string_view
case.
I've also asked a question @Twitter about preferences, here's the summary:
How do you init a string class member in a constructor? #cpp
- Bartlomiej Filipek (@fenbf) July 29, 2018
What do you think? Which one do you use in your code? Maybe there's some other option?
Published at DZone with permission of Bartłomiej Filipek, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments