Список инициализаторов С++ 11 сбой - но только в списках длины 2

Я выследил непонятную ошибку в протоколе к тому, что списки инициализаторов длины 2 выглядят как особый случай! Как это возможно?

Код был скомпилирован с помощью Apple LLVM версии 5.1 (clang-503.0.40), используя CXXFLAGS=-std=c++11 -stdlib=libc++.

#include <stdio.h>

#include <string>
#include <vector>

using namespace std;

typedef vector<string> Strings;

void print(string const& s) {
    printf(s.c_str());
    printf("\n");
}

void print(Strings const& ss, string const& name) {
    print("Test " + name);
    print("Number of strings: " + to_string(ss.size()));
    for (auto& s: ss) {
        auto t = "length = " + to_string(s.size()) + ": " + s;
        print(t);
    }
    print("\n");
}

void test() {
    Strings a{{"hello"}};                  print(a, "a");
    Strings b{{"hello", "there"}};         print(b, "b");
    Strings c{{"hello", "there", "kids"}}; print(c, "c");

    Strings A{"hello"};                    print(A, "A");
    Strings B{"hello", "there"};           print(B, "B");
    Strings C{"hello", "there", "kids"};   print(C, "C");
}

int main() {
    test();
}

Вывод:

Test a
Number of strings: 1
length = 5: hello

Test b
Number of strings: 1
length = 8: hello

Test c
Number of strings: 3
length = 5: hello
length = 5: there
length = 4: kids

Test A
Number of strings: 1
length = 5: hello

Test B
Number of strings: 2
length = 5: hello
length = 5: there

Test C
Number of strings: 3
length = 5: hello
length = 5: there
length = 4: kids

Я также должен добавить, что длина фиктивной строки в тесте b представляется неопределенной - она ​​всегда больше, чем первая строка инициализатора, но она варьируется от одной больше, чем длина первой строки, до общей длины две строки в инициализаторе.

Ответ 1

Введение

Представьте следующее объявление и использование:

struct A {
  A (std::initializer_list<std::string>);
};

A {{"a"          }}; // (A), initialization of 1 string
A {{"a", "b"     }}; // (B), initialization of 1 string << !!
A {{"a", "b", "c"}}; // (C), initialization of 3 strings

В (A) и (C) каждая строка c-стиля вызывает инициализацию одного (1) std::string, но, как вы сказали в своем вопросе, (B) отличается.

Компилятор видит, что возможно построить std::string с помощью begin и end-итератора, а при выражении разбора (B) он предпочтет такую ​​конструкцию с использованием "a" и "b" в качестве отдельных инициализаторов для два.

A { std::string { "a", "b" } }; // the compiler interpretation of (B)


Примечание. Тип "a" и "b" - это char const[2], тип, который может неявно распадаться на char const*, тип указателя, который подходит для действовать как итератор, обозначающий начало или конец, когда создает std::string. , но, мы должны быть осторожны: мы вызываем undefined потому что нет никакого (гарантированного) отношения между двумя указателями при вызове указанного конструктора.


Объяснение

Когда вы вызываете конструктор, принимающий std:: initializer_list, используя двойные фигурные скобки {{ a, b, ... }}, возможны две возможные интерпретации:

  • Внешние фигурные скобки относятся к самому конструктору, внутренние фигурные скобки означают, что элементы принимают участие в std:: initializer_list или:

  • Внешние фигурные скобки относятся к std:: initializer_list, тогда как внутренние фигурные скобки означают инициализацию элемента внутри него.

Предпочитает делать 2) всякий раз, когда это возможно, и поскольку std::string имеет конструктор с двумя итераторами, это тот, который вызывается, когда у вас есть std::vector<std::string> {{ "hello", "there" }}.

Дальнейший пример:

std::vector<std::string> {{"this", "is"}, {"stackoverflow"}}.size (); // yields 2

Решение

Не используйте для этой инициализации двойные фигурные скобки.

Ответ 2

Прежде всего, это поведение undefined, если я не пропущу что-то очевидное. Теперь позвольте мне объяснить. Вектор строится из списка строк инициализатора. Однако этот список содержит только одну строку. Эта строка формируется внутренним {"Hello", "there"}. Как? С конструктором итератора. По существу, for (auto it = "Hello"; it != "there"; ++it) формирует строку, содержащую Hello\0.

Для простого примера см. здесь. Хотя UB достаточно обоснован, казалось бы, второй литерал помещается сразу после первого в памяти. В качестве бонуса сделайте "Hello", "Hello", и вы, вероятно, получите строку длиной 0. Если вы ничего здесь не понимаете, я рекомендую читать отличный ответ Филиппа.