When classes have a constructor overload taking a std::initializer_list
, this overload will take precedence even if other constructor overloads are seemingly a better match. This problem is described in detail in Sutter's GotW#1, part 2, as well as Meyers' Effective Modern C++, Item 7.
The classic example of where this problem manifests itself is when brace-initializing a std::vector
:
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 to avoid class designs where a initializer_list
constructor overload can cause ambiguities to the programmer.
Sutter:
Guideline: When you design a class, avoid providing a constructor that ambiguously overloads with an initializer_list constructor, so that users won’t need to use ( ) to reach such a hidden constructor.
Meyers:
As a result, it’s best to design your constructors so that the overload called isn’t affected by whether clients use parentheses or braces. In other words, learn from what is now viewed as an error in the design of the std::vector interface, and design your classes to avoid it.
But neither of them describe how vector
should have been designed to avoid the problem!
So here's my question: How should have vector
been designed to avoid ambiguities with the initializer_list
constructor overload (without losing any features)?
I would take the same approach that the standard took with piecewise_construct
in pair
or defer_lock
in unique_lock
: using tags on 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.
};
That 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 value 20 or 20 elements of value 10, so the tag helps clarify that.
For the sake of completeness, one possible way (and not one I advocate) to avoid the ambiguity is to use static factory methods as a means to isolate the initializer_list
constructor from the others.
For example:
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) {/*...*/}
};
Usage:
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 is reminiscent of how objects are allocated and initialized in Objective-C. The nested Tag
struct 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 keep the other constructor overloads intact:
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) {/*...*/}
};
Usage:
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}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With