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 with auto.

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

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 even constexpr
  • 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.