dynamic
dynamic

Reputation: 48091

Different ways of calling an initializer-list-constructor

Consider this example for initializer-list-constructor usage:

std::vector<std::string> v = { "xyzzy", "plugh", "abracadabra" };
std::vector<std::string> v({ "xyzzy", "plugh", "abracadabra" });
std::vector<std::string> v{ "xyzzy", "plugh", "abracadabra" }; 

Are there any differences (even slightly) between them?

In a large project, where you have to define a standard, which style would you choose?
I would prefer the first style, the third can be easly confused with a call to a constructor with args. Also the first style looks familiar to other programming languages.

Upvotes: 9

Views: 255

Answers (1)

Praetorian
Praetorian

Reputation: 109089

In case of a vector of strings, there's no difference between the three forms. There can, however, be a difference between the first and the other two if the constructor taking the initializer_list is explicit. In that case, the first, which is copy-list-initialization, is not allowed, while the other two, which are direct-list-initialization, are allowed.

Because of that reason, my preference would be the third form. I'd avoid the second because the parentheses are redundant.


Further differences arise, as Yakk points out in the comments, when the type being constructed does not have a constructor taking an initializer_list.

Say for instance, the type being constructed has a constructor that takes 3 arguments, all of type char const *, instead of the initializer_list constructor. In that case, forms 1 & 3 are valid, but 2 is ill-formed, because the braced-init-list cannot match the 3 argument constructor when enclosed in parentheses.

If the type does have an initializer list constructor, but the elements of the braced-init-list are not implicitly convertible to initializer_list<T>, then other constructors will be considered. Assuming another constructor that is a match exists, form 2 results in an intermediate copy being constructed, while the other two don't. This can be demonstrated by the following example, compiled with -fno-elide-constructors.

struct foo
{
    foo(int, int) { std::cout << __PRETTY_FUNCTION__ << std::endl; }
    foo(foo const&) { std::cout << __PRETTY_FUNCTION__ << std::endl; }
    foo(std::initializer_list<std::string>) { std::cout << __PRETTY_FUNCTION__ << std::endl; }
};

int main()
{
    foo f1 = {1,2};
    std::cout << "----\n";
    foo f2({1,2});
    std::cout << "----\n";
    foo f3{1,2};
}

Output:

foo::foo(int, int)
----
foo::foo(int, int)
foo::foo(const foo&)
----
foo::foo(int, int)


The following case is not part of the question, but still good to be aware of. Using nested braces can result in unintuitive behavior in certain cases. Consider

std::vector<std::string> v1{{ "xyzzy", "plugh", "abracadabra" }};
std::vector<std::string> v2{{ "xyzzy", "plugh"}};

v1 works as expected and will be a vector containing 3 strings, while v2 results in undefined behavior. Refer to this answer for a detailed explanation.

Upvotes: 7

Related Questions