Constructing a vector of structs (with some custom constructors) from exactly two string literals crashes. Why?

Constructing a vector of structs (with some custom constructors) from exactly two string literals crashes. Why?


18

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

  • 4

    Change it to vector<X>{"a", "b", "c"}; to understand the problem. Change it to vector<X>{X{"a"}, X{"b"}}; to fix the problem.

    – Eljay

    yesterday

  • 4

    Debuggers are really cool. Here you could have stepped in and seen the program had landed in the wrong vector constructor.

    – user4581301

    yesterday

  • 2

    coliru.stacked-crooked.com/a/6c5042098aaab902 aha, Fascinating. I was also totally wrong about what was going on.

    – Mooing Duck

    yesterday

  • 3

    You'll love this one…

    – user4581301

    yesterday

  • 10

    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.

    – M.M

    yesterday

1 Answer
1


20

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 if InputIt 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 Voigt

    yesterday


  • 2

    @Jarod42 there's nothing to prevent the compiler from putting b before a.

    – Mark Ransom

    yesterday

  • 2

    @Sz. if those strings are being interpreted as iterators, they're iterators to char which probably triggers your int constructor. They aren't pointers to char* at that point.

    – Mark Ransom

    yesterday

  • 4

    @Sz. It's just unfortunate that the decay of a const char[] becomes a perfect iterator that fulfills the LegacyInputIterator requirement.

    – Ted Lyngmo

    yesterday

  • 2

    Another footnote: I think that the explicit for the int 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




Leave a Reply

Your email address will not be published. Required fields are marked *