Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does using uniform initializer syntax result in different behavior to the "old" style ()?

I get different results if I try to use a uniform initializer for std::set.

Example:

int main()
{
    std::array a {1,2,3,4};
    std::set<int> s1 {a.begin(), a.end()};
    std::set      s2 {a.begin(), a.end()};
    std::set      s3 (a.begin(), a.end());

    for(auto& i: s1) { std::cout << i << "\n"; }
    std::cout << "####" << std::endl;
    for(auto& i: s2) { std::cout << i << "\n"; }
    std::cout << "####" << std::endl;
    for(auto& i: s3) { std::cout << i << "\n"; }
}

Results in:

1   
2   
3   
4   
####
0x7ffecf9d12e0
0x7ffecf9d12f0
####
1   
2   
3   
4  

This seems to be related to "deduction guides", which are evaluated differently if used with {} or () syntax.

like image 590
Klaus Avatar asked May 26 '19 11:05

Klaus


People also ask

What is the point of uniform initialization in C++?

Uniform Initialization in C++ The uniform initialization is a feature that permits the usage of a consistent syntax to initialize variables and objects which are ranging from primitive type to aggregates. In other words, it introduces brace-initialization that applies braces ({}) to enclose initializer values.

What is uniform initialization?

Uniform initialization is a feature in C++ 11 that allows the usage of a consistent syntax to initialize variables and objects ranging from primitive type to aggregates. In other words, it introduces brace-initialization that uses braces ({}) to enclose initializer values.

What is the purpose of an initializer?

Initializer List is used in initializing the data members of a class. The list of members to be initialized is indicated with constructor as a comma-separated list followed by a colon. Following is an example that uses the initializer list to initialize x and y of Point class.

What are the advantages of initializer list?

The most common benefit of doing this is improved performance. If the expression whatever is the same type as member variable x_, the result of the whatever expression is constructed directly inside x_ — the compiler does not make a separate copy of the object.


1 Answers

Short answer

For s2, brace syntax is used, and {a.begin(), a.end()} is considered to be an initializer_list of std::array<int>::iterators. Therefore, s2 is a set of iterators.

For s3, parentheses syntax is used, and the iterator constructor is selected. s3 is a set of ints, and is initialized from the range [a.begin(), a.end()).

Long answer

Per [set.overview], we have two deduction guides relevant here:

template<class InputIterator,
         class Compare = less<typename iterator_traits<InputIterator>::value_type>,
         class Allocator = allocator<typename iterator_traits<InputIterator>::value_type>>
  set(InputIterator, InputIterator,
      Compare = Compare(), Allocator = Allocator())
    -> set<typename iterator_traits<InputIterator>::value_type, Compare, Allocator>;

and

template<class Key, class Compare = less<Key>, class Allocator = allocator<Key>>
  set(initializer_list<Key>, Compare = Compare(), Allocator = Allocator())
    -> set<Key, Compare, Allocator>;                                                

Per [over.match.class.deduct]/1:

A set of functions and function templates is formed comprising:

  • [...]

  • (1.4) For each deduction-guide, a function or function template with the following properties:

    • The template parameters, if any, and function parameters are those of the deduction-guide.

    • The return type is the simple-template-id of the deduction-guide.

In this case, the synthesized functions and function templates for the aforementioned deduction guides are, respectively,

template<class InputIterator,
         class Compare = less<typename iterator_traits<InputIterator>::value_type>,
         class Allocator = allocator<typename iterator_traits<InputIterator>::value_type>>
auto __func1(InputIterator, InputIterator,
            Compare = Compare(), Allocator = Allocator())
  -> set<typename iterator_traits<InputIterator>::value_type, Compare, Allocator>;

and

template<class Key, class Compare = less<Key>, class Allocator = allocator<Key>>
auto __func2(initializer_list<Key>, Compare = Compare(), Allocator = Allocator())
  -> set<Key, Compare, Allocator>; 

(I used double underscores to signify that these names are synthesized, and are not otherwise accessible.)


Per [over.match.class.deduct]/2:

Initialization and overload resolution are performed as described in [dcl.init] and [over.match.ctor], [over.match.copy], or [over.match.list] (as appropriate for the type of initialization performed) for an object of a hypothetical class type, where the selected functions and function templates are considered to be the constructors of that class type for the purpose of forming an overload set, and the initializer is provided by the context in which class template argument deduction was performed. Each such notional constructor is considered to be explicit if the function or function template was generated from a constructor or deduction-guide that was declared explicit. All such notional constructors are considered to be public members of the hypothetical class type.

The hypothetical class type looks like:

class __hypothetical {
public:
  // ...

  // #1
  template<class InputIterator,
           class Compare = less<typename iterator_traits<InputIterator>::value_type>,
           class Allocator = allocator<typename iterator_traits<InputIterator>::value_type>>
  __hypothetical(InputIterator, InputIterator,
                 Compare = Compare(), Allocator = Allocator())
    -> set<typename iterator_traits<InputIterator>::value_type, Compare, Allocator>;

  // #2
  template<class Key, class Compare = less<Key>, class Allocator = allocator<Key>>
  __hypothetical(initializer_list<Key>, Compare = Compare(), Allocator = Allocator())
    -> set<Key, Compare, Allocator>;

  // ...
};

For the declaration of s2,

std::set s2 {a.begin(), a.end()};

The overload resolution is performed as if in

__hypothetical __hyp{a.begin(), a.end()}; // braces

Thus, [over.match.list] comes in.

[...] overload resolution selects the constructor in two phases:

  • Initially, the candidate functions are the initializer-list constructors ([dcl.init.list]) of the class T and the argument list consists of the initializer list as a single argument.

  • [...]

Constructor #2 is an initializer-list constructor. Function template argument deduction gives

Key = std::array<int>::iterator

So the deduced type of s2 is

std::set<std::array<int>::iterator>

The declaration of s2 is equivalent to

std::set<std::array<int>::iterator> s2 {a.begin(), a.end()};

Therefore, s2 is a set of iterators that consists of two elements: a.begin() and a.end(). In your case, std::array<int>::iterator is probably int*, and a.begin() and a.end() happen to be serialized as 0x7ffecf9d12e0 and 0x7ffecf9d12f0, respectively.


For s3, overload resolution is performed as if in

__hypothetical __hyp(a.begin(), a.end()); // parentheses

That's direct-initialization, and is under the scope of [pver.match.ctor]. The initializer_list constructor is irrelevant, and the Constructor #1 is selected instead. Function template argument deduction gives

InputIterator = std::array<int>::iterator

So the deduced type of s3 is

set<iterator_traits<std::array<int>::iterator>::value_type>

Which is set<int>. Therefore, the declaration of s3 is equivalent to

std::set<int> s3 (a.begin(), a.end());

s3 is a set of ints that is initialized from the range [a.begin(), a.end()) — four elements 1, 2, 3, 4, which explains the output.

like image 54
L. F. Avatar answered Oct 07 '22 22:10

L. F.