Consider the following code snippet:
#include <vector>
#include <ranges>
#include <iostream>
struct A
{
};
struct B
{
B(void *) {};
};
template<class T, class R>
std::vector<T> foo(R &&range)
{
return std::vector<T> {
std::ranges::begin(range),
std::ranges::end(range)
};
}
int main()
{
std::cout << foo<A>(std::views::empty<A>).size() << "\n";
std::cout << foo<B>(std::views::empty<B>).size() << "\n";
}
It correctly prints 0
on the first line and incorrectly prints 2
on the second line on GCC 11.2 (https://godbolt.org/z/s6aoTGbr4) and MSVC 19.30.30705 (Visual Studio 2022 17.0).
Apparently in the second case std::views::empty
view produces iterators that causes constructor taking initializer list to be selected.
Changing std::vector<T> { ... }
to std::vector<T> ( ... )
fixes this, but I wonder if it is actually a bug in implementation (or even definition) of the empty
view?
CodePudding user response:
This is why you should be wary of list-initialization! In particular, this syntax:
std::vector<T>{ ... }
should only (!!) be used when you're specifically trying to invoke the std::initializer_list<T>
constructor, and specifically providing elements to the vector
and not in any other case. Like this one, where you're not trying to call that constructor.
The issue is that in list-initialization, the std::initializer_list
constructor is strongly preferred. If it is at all viable, it is chosen first. In this case, we are trying to construct a vector<B>
from two pointers to B
(that's the iterator type for empty<B>
). A B
is constructible from a B*
(via the void*
constructor), which makes the std::initializer_list<B>
constructor a viable candidate, which means it's selected.
As a result, you get a vector<B>
with two B
s, each constructed form a null pointer.
For A
, there's no such viable constructor, so you fallback to normal initialization, which will select the iterator pair constructor, giving you an empty vector<A>
.
The fix here is to use ()
s when you initialize, since that's actually what you intend to do.