Can you guess the output of this trivial program?
#include <vector>
#include <string>
#include <exception>
#include <iostream>
int main()
{
try {
struct X {
explicit X(int) {}
X(std::string) {} // Just to confuse you more...
};
std::vector<X>{"a", "b"};
} catch (std::exception& x) {
std::cerr << x.what();
}
}
Well, I couldn’t, which cost me a day of "research", before landing here, with finally having it distilled from some complex real-life code (with type aliases everywhere, anon. unions with non-POD members & hand-orchestrated ctors/dtors etc., just for the vibe).
And… I still can’t see what’s going on! Can someone please give a gentle hint? (Hopefully just a blind spot. I no longer do C++ professionally.)
Note: clean compile* with both (latest) MSVC /W4
and GCC -Wall
; same output in both (semantically).
* Even without the "confuse-the-reader" line. Which I think I’m gonna have nightmares from.
(Please bear with me for trying not to spoiler it by spelling everything out even more — after all, this truly is self-explanatory as-is, right? Except, the exact opposite for me…)
33
1 Answer
std::vector<X>{"a", "b"};
This creates a vector from two iterators of type const char*
using the constructor that takes two iterators:
template< class InputIt >
constexpr vector( InputIt first, InputIt last,
const Allocator& alloc = Allocator() );
Constructs the container with the contents of the range
[first, last)
.
This overload participates in overload resolution only ifInputIt
satisfies LegacyInputIterator, to avoid ambiguity with the overload (3). (below)
constexpr vector( size_type count,
const T& value,
const Allocator& alloc = Allocator() );
It’s just bad luck that the decay of the two const char[]
s becomes perfect iterators that fulfills the LegacyInputIterator requirement.
The iterators do not point to an array/contiguous area and the program therefore has undefined behavior.
What happens under the hood is most likely that it’ll try to get from the first const char*
to the second and run out of bounds as soon as its passing the null terminator after the 'a'
.
A similar construction that would actually work:
const char* arr = "working";
struct X {
explicit X(int i) {
std::cout << static_cast<char>(i);
}
};
const char* first = arr; // begin iterator
const char* last = arr + 7; // end iterator
std::vector<X>{first, last}; // prints "working"
15
-
2
@Sz.: From outside
std::vector::vector(begin, end)
the compiler doesn't know the two pointers need to be related. Inside, the compiler doesn't know that they aren't related.– Ben Voigtyesterday
-
2
@Jarod42 there's nothing to prevent the compiler from putting
b
beforea
.– Mark Ransomyesterday
-
2
@Sz. if those strings are being interpreted as iterators, they're iterators to
char
which probably triggers yourint
constructor. They aren't pointers tochar*
at that point.– Mark Ransomyesterday
-
4
@Sz. It's just unfortunate that the decay of a
const char[]
becomes a perfect iterator that fulfills the LegacyInputIterator requirement.– Ted Lyngmoyesterday
-
2
Another footnote: I think that the
explicit
for theint
ctor in the original example is another nicely reinforcing red herring here: it may well be surprising that even that can't stop the conspiracy of those implicit conversions…– Sz.yesterday
Change it to
vector<X>{"a", "b", "c"};
to understand the problem. Change it tovector<X>{X{"a"}, X{"b"}};
to fix the problem.yesterday
Debuggers are really cool. Here you could have stepped in and seen the program had landed in the wrong
vector
constructor.yesterday
coliru.stacked-crooked.com/a/6c5042098aaab902 aha, Fascinating. I was also totally wrong about what was going on.
yesterday
You'll love this one…
yesterday
One of many trip-wires introduced by "uniform initialization" . IMHO it was a mistake to re-use curly braces for this, and only added to the problems ; especially when it comes to aggregate initialization which is semantically different to non-aggregate, but now cannot be distinguished just from the syntax.
yesterday