Last Update:
20+ Ways to Init a String, Looking for Sanity
Table of Contents
C++ is famous… or infamous for its complex initialization syntax. In this article, I’ll show you around 20 ways to initialize simple std::string
variables. Can we somehow make it easier to understand?
Default values
Have a look:
void foo() {
std::string str0;
std::string str1 {};
}
We have two local variables (with automatic storage duration), str0
is default initialized, while str1
is value initialized.
While default initialization is unsafe for integers or other built-in types, it’s relatively fine (but less readable) for class types with a default constructor. In that case, a default constructor will be called, and the string object will get an empty value. The same constructor is invoked for value initialization.
Nevertheless, it’s best to set a value:
void foo() {
int x; // wrong code!! indeterminate value!!
int y {}; // better, y == 0
int z { 0 }; // even more explicit and easier to read
int w = 0; // also fine
std::string str {}; // empty and looks fine
}
See the supportive C++ core guideline: C++ Core Guidelines - ES.20: Always initialize an object.
Copy vs. direct
Usually, it’s best to wait until there’s some value. In a case of a simple integer, we have several forms:
int x (10.2); // direct
int y = 10.2; // copy
int x2 { 20.2 }; // direct list initialization
int y2 = { 20.2 }; // copy list initialization
While it may look strange that I assign a double value to an integer, the point is that lines with x2
and y2
won’t compile. List initialization prevents narrowing conversions. Have a look at Compiler Explorer.
The same happens for computing value in a helper function (see @Compiler Explorer):
double computeParam() { return 10.2; }
int main() {
int paramX (computeParam());
int paramY = computeParam();
int paramX2 { computeParam }; // error
int paramY2 = { computeParam() }; // error
}
For strings, we have several options:
std::string str2 ("Hello World ");
std::string str3 = "Hello World";
And its variation with list syntax:
std::string str4 {"Hello World "};
std::string str5 = {"Hello World"};
In all cases, the compiler will call the single constructor:
constexpr basic_string( const CharT* s,
const Allocator& alloc = Allocator() );
What’s more, the copy syntax doesn’t consider so-called explicit
constructors:
struct S {
explicit S(int x) : v_(x) { }
int v_ { 0 };
};
int main() {
// S s = 10; // error!
S s2 (10); // fine
S s3 {10}; // fine
}
For strings, we have, for example an explicit constructor for string_view
:
template<class StringViewLike>
explicit constexpr basic_string(const StringViewLike& t,
const Allocator& alloc = Allocator() );
See an example: (run here):
#include <string>
#include <string_view>
int main() {
std::string_view sv { "Hello World" };
// std::string str6 = sv; // error!
std::string str7 {sv};
}
Braces or not?
Is it better to call braces or regular round parens? Have a look at the following example:
#include <iostream>
int main() {
std::string str8(40, '*'); // parens
std::string str9{40, '*'}; // <<
std::cout << str8 << '\n';
std::cout << str9 << '\n';
}
The output:
****************************************
(*
In the second case, we call:
constexpr basic_string( std::initializer_list<CharT> list,
const Allocator& alloc = Allocator() );
List initialization has this unwanted consequence that tries to convert a range of values into a single initializer_list
(when there’s a constructor taking such an object). If you want to call some special constructor for a container, it’s best to use ()
as it uses a “regular” function overload call and doesn’t treat initializer_list
in a special way.
Non-local scopes
If we move out of the function scope, we can think about at least several options:
// in some file.cpp (not a header)
std::string str10; // static storage, external linkage
const std::string str11 { "abc" }; // static storage, internal linkage
static std::string str12; // static storage, internal linkage
inline std::string str13; // static storage, external linkage
namespace lib {
std::string str14; // static storage, external linkage
}
namespace {
std::string str15; // static storage, internal linkage
}
void foo() {
static std::string str16; // static inside a function scope
}
struct Test {
int x { 0 };
static inline std::string s17; // external linkage
};
The code above doesn’t include module linkage options that we also get with C++20.
As for the initialization, process strings will go through the “dynamic initialization” step for static variables. For trivial types, there can also be constant initialization taking place or zero initialization:
For example:
int x; // zero initialization, but please don't try!
int y = 10; // constant initialization
void foo() { }
See my other blog post: What happens to your static variables at the start of the program? - C++ Stories.
Deduction
So far, I explicitly mentioned the type of variables, but we can use auto x =
form:
auto str18 = std::string{"hello world"};
auto str19 = "hello world"s;
auto computeString(int x) {
return std::to_string(x);
}
const auto str20 = computeString(42);
What’s the best form?
C++11 introduced list initialization which tried to become “uniform” initialization. One syntax for all options. Being “uniform” is not that easy, especially taking various historical reasons and C-language compatibility. It’s better with each revision of C++, but there are some exceptions.
C++ Core Guidelines suggests: the following rule “ES.23: Prefer the {}
-initializer syntax”
Reason
Prefer
{}
. The rules for{}
initialization are simpler, more general, less ambiguous, and safer than for other forms of initialization. Use=
only when you are sure that there can be no narrowing conversions. For built-in arithmetic types, use=
only withauto
.Exception
For containers, there is a tradition for using
{...}
for a list of elements and(...)
for sizes:
As you can see, there are lots of options for static variables. In this case, inline
variables introduced in C++17 can help greatly. What’s more, it’s best to avoid global state, so think twice if you really have to create such an object.
Additional guides
- In Item 7 for Effective Modern C++, Scott Meyers said that “braced initialization is the most widely usable initialization syntax, it prevents narrowing conversions, and it’s immune to C++’s most vexing parse.
- Nicolai Josuttis had an excellent presentation about all corner cases: CppCon 2018: Nicolai Josuttis “The Nightmare of Initialization in C++” - YouTube, and suggests using
{}
- Only abseil / Tip of the Week #88: Initialization: =, (), and {} - prefers the old style. This guideline was updated in 2015, so many things were updated as of C++17 and C++20.
- In Core C++ 2019 :: Timur Doumler :: Initialisation in modern C++ - YouTube - Timur suggests
{}
for all, but if you want to be sure about the constructor being called then use()
. As()
performs regular overload resolution.
Bonus
There’s also at least one other way to initialize data:
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
int main() {
using namespace std::string_literals;
const std::vector<std::string> vs = { "apple", "orange",
"foobar", "lemon" };
const auto prefix = "foo"s;
auto result = std::find_if(vs.begin(), vs.end(),
[&prefix](const std::string& s) {
return s == prefix + "bar"s;
}
);
if (result != vs.end())
std::cout << prefix << "-something found!\n";
result = std::find_if(vs.begin(), vs.end(),
[savedString = prefix + "bar"s](const std::string& s) {
return s == savedString;
}
);
if (result != vs.end())
std::cout << prefix << "-something found!\n";
}
Have a look at savedString
. It uses a capture clause with an initializer, available since C++14 for lambda expressions. Here’s a note from my book on that feature:
Now, in C++14, you can create new data members and initialize them in the capture clause. Then you can access those variables inside the lambda. It’s called capture with an initializer, or another name for this feature is generalized lambda capture.
So, savedString
is technically a data member of an anonymous callable object, but the syntax is quite cool.
Summary
While we can easily come up with a lot of techniques and strange syntax for initialization, I think there’s also a simple way to look at it:
- Always initialize variables; use
{}
to value initialize them at least const
if posible, or evenconstexpr
- use list initialization unless you want to call some specific constructor (like for containers and setting the size)
- limit the number of global objects
We also haven’t discussed arrays and compounds (in C++20, you can use Designated Initializers (see my post)).
Also, please look at a popular blog post from 2017 Initialization in C++ is bonkers where you can find at least 18 different ways to initialize an integer.
Back to you
- Can you add some other ways to init a string?
- What are your tactics for variable initialization?
- is this an important topic for you? or do you not care much?
Please leave a comment below.
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: