How to create classes with constructor accepting std :: initializer_list?

When classes have constructor overloads with std::initializer_list , this overload will take precedence, even if other constructor overloads seem more appropriate. This issue is described in detail in Sutter GotW # 1 , part 2, as well as Meyers Effective Modern C ++ , clause 7.

A classic example of how this problem manifests itself is when std::vector braces are initialized:

 std::vector<int> vec{1, 2}; // Is this a vector with elements {1, 2}, or a vector with a single element 2? 

Both Sutter and Meyers recommend avoiding class constructions where overloading the initializer_list constructor can cause inaccuracies for the programmer.

Sutter:

Leadership. When you create a class, avoid creating a constructor that ambiguously overloads the initializer_list constructor, so users do not need to use () to access such a hidden constructor.

Meyers:

As a result, it is best to design your constructors so that the overload caused does not depend on whether clients use parentheses or braces. In other words, find out what is now seen as a mistake in the design of the std :: vector interface and the design of your classes to avoid this.

But not one of them describes how vector should have been designed to avoid the problem!

So, here is my question: How should vector be in order to avoid ambiguities with the construction of the initializer_list constructor (without losing any functions)?

+6
source share
2 answers

I would take the same approach as the standard with piecewise_construct in pair or defer_lock in unique_lock : using tags in the constructor:

 struct n_copies_of_t { }; constexpr n_copies_of_t n_copies_of{}; template <typename T, typename A = std::allocator<T>> class vector { public: vector(std::initializer_list<T>); vector(n_copies_of_t, size_type, const T& = T(), const A& = A()); // etc. }; 

In this way:

 std::vector<int> v{10, 20}; // vector of 2 elems std::vector<int> v2(10, 20); // error - not a valid ctor std::vector<int> v3(n_copies_of, 10, 20); // 10 elements, all with value 20. 

Plus, I always forget if it's 10 elements of a value of 20 or 20 elements of a value of 10, so the tag helps clarify this.

+8
source

One possible way to avoid ambiguity is to use static factory methods as a means of isolating the initializer_list constructor from others.

For instance:

 template <typename T> class Container { public: static Container with(size_t count, const T& value) { return Container(Tag{}, count, value); } Container(std::initializer_list<T> list) {/*...*/} private: struct Tag{}; Container(Tag, size_t count, const T& value) {/*...*/} }; 

Using:

 auto c1 = Container<int>::with(1, 2); // Container with the single element '2' auto c2 = Container<int>{1, 2}; // Container with the elements {1, 2} 

This static factory approach recalls how objects are allocated and initialized in Objective-C . The nested Tag structure is used to ensure that the initializer_list overload is not viable.


Alternatively, the initializer_list constructor can be changed to a static factory method, which allows you to save other constructor problems:

 template <typename T> class Container { public: static Container with(std::initializer_list<T> list) { return Container(Tag{}, list); } Container(size_t count, const T& value) {/*...*/} private: struct Tag{}; Container(Tag, std::initializer_list<T> list) {/*...*/} }; 

Using:

 auto c1 = Container<int>{1, 2}; // Container with the single element '2' auto c2 = Container<int>::with({1, 2}); // Container with the elements {1, 2} 
+1
source

Source: https://habr.com/ru/post/982110/


All Articles